Deprecate binary en/he TextChunk for real-language TextChunk - #3669
Deprecate binary en/he TextChunk for real-language TextChunk#3669YishaiGlasner wants to merge 9 commits into
Conversation
…e TextRange Finishes the TextChunk -> TextRange migration: gives TextRange save support, moves the save path and all live GET call sites over to it, retires the alt-structure text preview, and updates the legacy editor so saving no longer assumes only "en"/"he". TextRange is then renamed to TextChunk; the old binary en/he TextChunk is renamed to LegacyTextChunk and relocated to sefaria/model/legacy_text.py along with TextFamily and VirtualTextChunk, which now back only the legacy v1 texts API, parashat_hashavua, and other confirmed-dormant call sites -- imported locally and marked as legacy rather than exported repo-wide. Along the way, fixes two latent bugs in merge_texts() (lost positional correspondence for depth>2 texts, fake version attribution for empty positions), closes a version-title-suffix desync that could silently create duplicate Version records on non-en/he saves, and de-duplicates the lang<->direction legacy-bucket conversion behind two shared helpers in sefaria/constants/model.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
I'll analyze this PR systematically, reviewing the code changes, architecture, implementation quality, and test coverage. PR OverviewThis PR migrates Sefaria's text access layer from a binary Issues Found1. Potential Bug:
|
There was a problem hiding this comment.
Pull request overview
This PR migrates Sefaria’s text read/write path away from the legacy binary "en"/"he" TextChunk model to a real-language-aware TextChunk (using actualLanguage + languageFamilyName + direction), and updates the editor and major call sites accordingly while retaining legacy API surfaces via a new legacy_text module.
Changes:
- Replaces the old binary
TextChunkwith a new real-languageTextChunk(read + save), updatesRef.text()/tracker.modify_text()to use it, and adjustsVersionuniqueness to includedirection. - Moves legacy read-path classes (
LegacyTextChunk,TextFamily, etc.) intosefaria/model/legacy_text.pyand migrates most internal callers todirection=/actualLanguagesemantics. - Updates the editing UI to select ISO language + direction, and adds/updates regression tests around merging/sources attribution and editor response shape.
Reviewed changes
Copilot reviewed 51 out of 51 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| templates/edit_text.html | Replaces hardcoded en/he language picker with ISO-language select and adds a direction picker. |
| static/js/s1/editor.js | Populates ISO language list in the editor; uses versions metadata (isPrimary/isEdited, actualLanguage, direction) for edit/compare flows. |
| static/js/ConnectionsPanel.jsx | Updates edit URL construction to use languageFamilyName instead of en/he. |
| static/css/reader.css | Hides the new direction selector for “Original Translation” mode. |
| sourcesheets/views.py | Switches sheet source text retrieval to Ref.text(direction=...) while preserving en/he sheet semantics. |
| sefaria/views.py | Updates bundled-text code paths to use direction/actual-language selection instead of legacy chunks. |
| sefaria/urls_shared.py | Adds clarifying “legacy” comments for older endpoints. |
| sefaria/urls_library.py | Updates edit URL route to accept language_family_name instead of 2-letter lang. |
| sefaria/tracker.py | Routes saves through new TextChunk(actual_lang=..., direction=...) and threads direction through modify path. |
| sefaria/tests/search.py | Updates search test to use LegacyTextChunk where legacy behavior is required. |
| sefaria/tests/search_dependencies_test.py | Updates test fixtures to use LegacyTextChunk and clarifies VersionState refresh behavior. |
| sefaria/tests/links_test.py | Adds regression test around link version metadata attribution when text is empty at a position. |
| sefaria/sheets.py | Keeps dormant legacy flows on LegacyTextChunk with explicit legacy imports. |
| sefaria/search.py | Indexing pulls text via new TextChunk keyed by direction derived from legacy lang. |
| sefaria/model/user_profile.py | Recently-read preview now uses ref.text(direction=...) instead of legacy chunk reads. |
| sefaria/model/text.py | Core migration: new TextChunk, Ref.text() signature changes, Version.pkeys includes direction, fixes to merge_texts() attribution nesting. |
| sefaria/model/text_request_adapter.py | Uses new TextChunk in v3 adapter, refines merge/sources reporting, and reduces VersionState DB reads. |
| sefaria/model/tests/text_test.py | Updates tests to use LegacyTextChunk where appropriate after rename/migration. |
| sefaria/model/tests/text_request_adapter_test.py | New tests covering v3 adapter “sources” behavior (no false merges; still flags real merges). |
| sefaria/model/tests/schema_test.py | Updates schema tests to use LegacyTextChunk in legacy expectations. |
| sefaria/model/tests/index_schema_test.py | Updates index schema tests to use LegacyTextChunk. |
| sefaria/model/tests/index_offsets_by_depth_tests.py | Imports legacy TextFamily for tests that still rely on it. |
| sefaria/model/tests/chunk_test.py | Adds regression tests for merge attribution correctness and vtitle suffixing on save; updates legacy chunk usage. |
| sefaria/model/schema.py | Removes dead alt-structure preview generation previously routed through legacy text objects. |
| sefaria/model/marked_up_text_chunk.py | Validates MUTCs using oref.text(direction=...) instead of legacy chunk construction. |
| sefaria/model/linker/tests/linker_test.py | Uses LegacyTextChunk in linker validation test fixtures. |
| sefaria/model/legacy_text.py | New module containing LegacyTextChunk, VirtualTextChunk, and TextFamily moved out of text.py. |
| sefaria/model/garden.py | Keeps garden feature on LegacyTextChunk explicitly. |
| sefaria/model/init.py | Stops exporting legacy classes from sefaria.model root import surface. |
| sefaria/image_generator.py | Refactors legacy lang→direction conversion via shared helper. |
| sefaria/history.py | Reads revision text via direction-derived TextChunk for legacy history language keying. |
| sefaria/helper/text.py | Changes helper APIs from lang to direction for write operations and threads direction into modify/save flows. |
| sefaria/helper/tests/text_test.py | Updates helper tests to match the new helper API signatures and legacy chunk usage. |
| sefaria/helper/tests/schema_test.py | Updates schema helper tests to use LegacyTextChunk. |
| sefaria/helper/tests/auto_linking_test.py | Updates auto-linking tests to use LegacyTextChunk where the legacy path is required. |
| sefaria/helper/schema.py | Constructs new TextChunk directly from Version metadata (family + direction) during schema migrations. |
| sefaria/helper/marked_up_text_chunk_generator.py | Loads segment text via direction derived from legacy lang. |
| sefaria/helper/linker/linker.py | Linker text fetch now uses oref.text(direction=...) merge behavior rather than legacy fallback mode. |
| sefaria/helper/linker/disambiguator.py | Disambiguator uses direction-based fallback behavior when scoring candidate refs. |
| sefaria/helper/link.py | Removes dependency on TextFamily for link-building logic, using VersionState/ref emptiness checks instead. |
| sefaria/export.py | Updates merged export metadata collection to flatten nested merged source structures. |
| sefaria/datatype/jagged_array.py | Updates comments to reference LegacyTextChunk.trim_text as the source. |
| sefaria/constants/model.py | Adds shared helpers for legacy lang ↔ direction conversion. |
| sefaria/client/wrapper.py | get_links(with_text=1) migrated off TextFamily and improves version attribution handling for empty positions. |
| scripts/replace_stuma_and_ptuha.py | Updates helper call sites to pass direction instead of legacy lang. |
| scripts/remove_marks_from_tyt.py | Updates helper call sites to pass direction instead of legacy lang. |
| scripts/fix_rashi_double_quotes.py | Updates helper call sites to pass direction instead of legacy lang. |
| scripts/find_replace.py | Updates helper call sites to pass direction instead of legacy lang. |
| reader/views.py | Editor view now uses v3 adapter response shape and threads direction into legacy v1 POST saves; social image API migrated off TextFamily. |
| reader/tests/social_image_api_test.py | Updates social image tests to mock the new ref.text(...) path rather than TextFamily. |
| reader/tests.py | Adds regression tests for edit_text() response flags/metadata; updates remaining uses to legacy chunk where required. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for v in versions: | ||
| if v.versionTitle == vtitle: | ||
| return v | ||
| return max(versions, key=lambda v: getattr(v, 'priority', 0)) |
…=None Version._normalize() can set priority to None on a float() parse failure, not just leave it missing -- getattr's default only covers the latter, so max() would raise TypeError comparing None to a number. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Continuous workflow never ran for this branch (no check runs on either commit), so no images were ever pushed for the new-tc cauldron, leaving it stuck in ImagePullBackOff.
…t-ui-for-admins-and # Conflicts: # sefaria/model/text.py
…-assignment-ui-for-admins-and' into feature/sc-46004/expose-language-assignment-ui-for-admins-and
| for span in spans: | ||
| assert span['ambiguous'] == is_ambiguous | ||
| validation_text = TextChunk(Ref("Genesis 1:1"), lang="en", vtitle="Tanakh: The Holy Scriptures, published by JPS").text | ||
| validation_text = LegacyTextChunk(Ref("Genesis 1:1"), lang="en", vtitle="Tanakh: The Holy Scriptures, published by JPS").text |
There was a problem hiding this comment.
You can use the new TextChunk here. No reason not to.
| lang = version.language | ||
| priority = version.priority | ||
| content = TextChunk(oref, lang, vtitle=vtitle).ja().flatten_to_string() | ||
| content = LegacyTextChunk(oref, lang, vtitle=vtitle).ja().flatten_to_string() |
There was a problem hiding this comment.
why use LegacyTextChunk here?
…tics Both failures are caused by this PR's real-language changes interacting with fixtures/assertions written under the old binary en/he model -- not regressions in the tests' own original intent: - test_rename_collision_different_language_family_allowed: a non-en/he version now gets its versionTitle auto-suffixed (e.g. "... [de]") by Version._normalize() on save, specifically to prevent it from colliding with a same-direction version sharing that literal title. The test asserted the pre-suffix literal string; updated to assert the real behavior (only one version can hold the literal title, the other gets the bracket suffix), which is closer to what the test was checking for in the first place. - test_index_rename_migrates_versions: its fixture inserts Version docs via raw db.texts.insert_many() to skip Version._validate(), bypassing Version._normalize() too -- so actualLanguage/languageFamilyName were never backfilled from the legacy language field. Ref.text(lang=...) now matches on those real-language fields, not the legacy binary one; added them to the fixture to match what _normalize() would have produced. The other 3 tests reported as CI failures (test_usage_index_surgical_add_remove, test_hooks_noop_when_search_index_on_save_off, test_category_hook_empty_new_path_guard) are unrelated pre-existing breakage (confirmed failing on prod/master independent of this branch) and are intentionally left untouched here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
stevekaplan123
left a comment
There was a problem hiding this comment.
Can you add to the PR a description of why we need LegacyTextChunk? I see that it's used in gardens, but I find its existence confusing.
…ff LegacyTextChunk TextChunk (the new real-language class) had only narrow, bug-specific tests; the comprehensive read/save-path coverage (verse/chapter/range/spanning reads, depth-1/3 addressing, blank/extend/overwrite saves, HTML sanitization) only existed for LegacyTextChunk, which is no longer the live save path. Adds six new tests mirroring that coverage for TextChunk, all synthetic fixtures. LegacyTextChunk's own tests are untouched. Also updates three test files that still built fixtures via LegacyTextChunk even though the production code they exercise (search.py's index_ref, search_dependencies_test.py's book fixture, a linker validation helper) was already migrated to TextChunk -- for consistency with what's actually being tested, not a behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only the legacy v1 texts_api GET (external third-party consumers) is a real, must-keep dependency. Everything else that still technically routes to this class -- bulktext_api's ?useTextFamily=1, Garden's visual-garden pages, parashat_hashavua_api, sheets.py's rebuild_sheet_nodes/ refine_ref_by_text -- is not actively maintained product surface, kept only because it hasn't been removed yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@stevekaplan123 added comment to LegacyTextChnk and edoted desc |
Direction is no longer a separate control in the legacy editor -- it's derived from the selected language's default (Sefaria.ISOMap.defaultDirection) for a brand-new version. Editing an existing version never recomputes or resends direction at all, so a version whose direction was deliberately saved as an exception to its language's default (e.g. a transliterated Arabic/Persian version) is left untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Naming note: the new real-language class was originally called
TextRangeand renamed toTextChunkpartway through this work, with the old binary class becomingLegacyTextChunk. This description uses the final names throughout —TextChunkalways means the new, real-language class;LegacyTextChunkalways means the old binary en/he one.LegacyTextChunkwas built on a binary en/he assumption: a version's real language (Yiddish, French, Judeo-Arabic, etc.) got forced into an"en"/"he"bucket by direction, so a German translation stored aslanguage="en"would be returned to callers as if it were literally English. This PR does two things.1. Gives the new
TextChunksave support and wires it end-to-end, including the editorTextChunk.save()mirrorsLegacyTextChunk.save()'s architecture directly — it inheritsAbstractTextRecordand reuses_validate,_sanitize,_trim_ending_whitespace,_check_available_text_pre_save,_update_link_language_availabilityunchanged.sefaria/tracker.py'smodify_text— the shared entry point for every text save — now constructs the newTextChunk(actual_lang/directionthreaded through from its own newdirectionkwarg) instead of the old one; this is where the save path actually moved. What's different, and why:directionandactual_lang, alongsidelang/vtitle.lang(languageFamilyName) can now be derived fromactual_langvia the existingLANGUAGE_CODESmap, so a caller creating a brand-new version only needs to supply the real ISO code an editor picked — not compute both independently.directionis a new lookup/save dimension, and also the source for the legacylanguagefield ("he" if direction == "rtl" else "en") when a brand-newVersionis created, since that field still has to satisfy the old binary schema underneath.LegacyTextChunkwas saveable wheneverlangandvtitlewere both given, gated further byfallback_on_default_version/exclude_copyrightedto guard its implicit/fuzzy vtitle-guessing mode. The newTextChunknever has that fuzzy mode — it's saveable whenevervtitleis given plus eitherdirectionorlang(not both required), with no fallback/guessing params. (exclude_copyrightedwas also confirmed dead onLegacyTextChunkitself — never passedTrueanywhere — and dropped fromRef.text()'s public signature entirely; grepped both this branch and master for any caller passing it, none found.)Version.pkeyschanged from["title", "versionTitle"]to["title", "direction", "versionTitle"]. This isn't a hypothetical concern: the live data already has real collisions on the old key. We found 60(title, versionTitle)pairs that currently exist as both anltrand anrtlversion — e.g. "Rashi on Psalms" / "Wikisource Mikraot Gedolot" exists in both directions. Whether that data pattern itself is desirable is a separate question this PR doesn't touch — we didn't create or change it, we only changedpkeysto correctly reflect the uniqueness boundary the data already has, instead of one that was already silently violated.TextChunk._validate()is narrower thanLegacyTextChunk._validate()— it only checks that the posted text's depth matches the ref, dropping the old spanning-ref/range-length validation branches entirely. This is safe, not an oversight:_saveableexcludesis_range()refs outright, so a spanning or range ref can never reachsave()on the new class in the first place — there's nothing left for those branches to validate.[xx]-versionTitle-suffix convention is kept, not retired —db.historyand some legacy version-grouping code still key on the coarse(language, versionTitle)pair, so a German version forced into thelanguage="en"bucket needs the suffix to avoid colliding with an actually-English version sharing that title. It's now generated automatically on save instead of something an editor had to type manually. Because the save response doesn't otherwise expose the possibly-auto-suffixed title,TextChunk.save()setsself.vtitlefrom the savedVersion's real title before returning, and the save API echoes it back in its JSON response — without this, a caller holding the pre-suffix title (e.g. a second save in the same session, or a script doing sequential per-segment saves) could create a colliding duplicate version.editor.js/edit_text.html): the hardcoded<select>with two options (en/he) is replaced with an ISO-code picker driven bySefaria.ISOMap(same source used elsewhere in the client). There's no separate direction control: direction is derived fromSefaria.ISOMap[lang].defaultDirectionfor a brand-new version, and left completely alone when editing an existing one, so a version whose direction was deliberately saved as an exception to its language's default (a handful of real transliterated Arabic/Persian versions) is never silently overwritten. Adding/editing a "Sefaria Community Translation" hides the language picker and forces English, since that identity is fixed by definition. The/edit/<ref>/<lang>/<version>URL and its client-side builder (sefaria/urls_library.py's route regex,ConnectionsPanel.jsx's edit-link construction) both moved from a 2-letter\w\w(en/he only) segment to a fulllanguageFamilyNamestring, to carry a real language through the URL instead of a binary code.2. Renames the classes, moves the legacy ones out, and migrates every live caller to the new
TextChunkThis starts with the rename itself —
TextRange→TextChunk, oldTextChunk→LegacyTextChunk— and physically relocatingLegacyTextChunk,VirtualTextChunk(its delegate for virtual/dictionary nodes), andTextFamilyinto a newsefaria/model/legacy_text.py, no longer exported fromsefaria/model/__init__.py.TextFamilyis worth calling out specifically — it was the bridge/assembly class that built the full legacy response shape (text plus commentary, links, and version metadata bundled together) for the old v1 texts API and everything that rendered off of it. It was a heavily-used object across the old GET path, not a minor helper. It's kept around for the legacy v1texts_apiGET (external third-party consumers) — see the class docstring onLegacyTextChunkfor the full breakdown of what else still technically routes to it and why.reader/views.py'ssocial_image_apiis fully migrated offTextFamilyentirely (ref.padded_ref().text(direction=...)) — it no longer needs it at all. (sefaria/image_generator.pyis unrelated to this migration — its only change in this PR is folding an existing ternary into the shareddirection/langhelper described below; it never usedTextChunk/TextFamily.)With the rename and relocation in place, every live caller of the old class is migrated. Three categories:
Migrated to
direction=— deliberately preserving the old bucket-based behavior, for call sites whose output is implicitly a closed "he"/"en" pair or that are legacy-keyed and shouldn't change behavior:client/wrapper.py'sget_links(),linker.py's_get_ref_text_by_lang_for_linker,marked_up_text_chunk.py,marked_up_text_chunk_generator.py,disambiguator.py,sourcesheets/views.py'sget_correct_text_from_source_obj,reader/views.py'ssocial_image_api,history.py'stext_at_revision(matchesdb.history's own legacylanguage-field keying),search.py'sindex_ref,user_profile.py's recently-read text preview.Migrated to real
actualLanguagematching — deliberately closing the leak, where the caller's intent was genuinely "real English" or "real Hebrew," not "whatever's in the direction bucket":Ref.word_count()(no live callers found anywhere, migrated anyway rather than leaving a redundant legacy-only path around).api/views.py'sKnnSearch._ref_textalso belongs here, with a wrinkle worth flagging explicitly: its diff against master is empty. The call site readsRef(ref).text(lang="he"), textually identical to the line on master — butlang=means something different now (exactactualLanguagematch, not the old positional binary bucket), so its behavior differs from master even though nothing shows up in a file diff.Moved away from language-bucketed logic entirely — behavior unchanged:
_build_links_internal(sefaria/helper/link.py) no longer touchesTextChunk/TextFamilyat all — itsTextFamily(...).contents()-based non-emptiness check is replaced withRef.is_empty(vstate=vstate)/get_state_ja(vstate=vstate)/get_subrefs_count(...), none of which take a language argument. This is equivalent to the old behavior, but not because the new check is language-agnostic — it isn't.VersionState's_allmask (sefaria/model/version_state.py:219) is literally_en + _he, built from the same legacyVersion.languagefieldTextFamilyread from. Both the old and new checks are equally blind to real non-en/he-languaged versions; this migration just stops routing that (pre-existing, unchanged) blind spot through legacy field names. Two structural helpers insefaria/helper/schema.py(used when converting between simple/complex text structures) similarly construct the newTextChunkdirectly from an already-loadedVersion's ownlang/direction/vtitle— no bucket decision needed there at all, since the version is already known.Completely removed: the alt-structure text preview (
SchemaNode.as_index_contents()'swholeRefPreview/refsPreviewgeneration,expand_ref) — confirmed dead, its only consumer was the pre-Reacttemplates/js/headers.js, itself gated behind an already-unused flag.Marked as legacy, not removed (beyond
TextFamilyabove): it's for the v1 API — see theLegacyTextChunkclass docstring (sefaria/model/legacy_text.py) for what else still technically routes to it and why none of that is a real reason to keep the class around. The legacy v1texts_apiPOST now derives a sensibledirectionfromlanguagewhen a caller doesn't send it, so older API clients keep working without needing to be updated themselves.LegacyTextChunk's own internal availability-check helpers constructLegacyTextChunkdirectly rather than going through the newRef.text(), so they keep matching the whole legacy bucket rather than one exact language.Won't work, for a different reason than it might look like: four scripts (
find_replace.py,fix_rashi_double_quotes.py,remove_marks_from_tyt.py,replace_stuma_and_ptuha.py) were updated — not because they constructTextChunkdirectly, but becausesefaria/helper/text.py'sfind_and_replace_in_text()/modify_text_by_function()had their positional signature changed (lang→direction) as real Phase 1/2 migration work, and these scripts call those functions directly. Without updating them, they'd silently pass"he"where"rtl"is now expected. Separately, roughly 20 other one-off analysis scripts underscripts/still constructTextChunk(oref, "he"/"en", vtitle)directly, using the pre-rename 3-positional-argument convention — sinceTextChunknow refers to the new class, the literal"he"/"en"is read aslanguageFamilyNameand matches nothing. These were left as-is: none are wired into any scheduled job, only invoked by hand, so updating them was judged not worth the review burden until someone actually needs to run one.Deviation from the original plan worth flagging:
sefaria/helper/text.py'ssplit_text_sectionwas planned for deletion (confirmed zero external callers — only its own recursive self-call). It was migrated instead of deleted. Still zero callers as of this PR — a candidate for actual removal as a follow-up.Bugs fixed (pre-existing, not introduced by this PR)
merge_texts()had two bugs, both already reachable in production throughLegacyTextChunk._choose_version_by_lang(used throughout the app for any multi-version merge): depth>2 merges flattened the source-attribution list across the entire node, losing which position each source belonged to (the code's own comment already flagged this: "the mapping of source names to segments is lost for merged texts of depth > 2"); and empty positions defaulted to attributing the first candidate version instead of being left unattributed, fabricating version credit for content that version never actually had. Fixed by preserving nesting through the merge and using an explicit "no attribution" sentinel for empty positions.export.py's merge visitor (used for exporting a merged text) was updated to match the corrected, now-nested shape.TextRequestAdapter._append_version, read by the client ascurrentVersion.merged = !!(currentVersion.sources)) whenever 2+ candidate versions existed for a language, even when the requested version alone was already complete for that ref — surfacing a misleading "merged" badge on an ordinary single-version read. Fixed to only report it on genuine multi-version attribution.Performance
TextRequestAdapter._add_ref_data_to_return_objnow shares a singleVersionStatefetch to compute next/prev section links, instead of two independent uncached DB round-trips. This isn't editor-specific —_add_ref_data_to_return_objis shared byedit_text, the/api/v3/textsendpoint, and the main reader's own SSR panel loading (make_panel_dict), so this benefits ordinary reader page loads too, not just the editor.Testing
merge_texts()'s positional-correspondence/no-fake-attribution fix (chunk_test.py), the version-title-suffix save fix (chunk_test.py), the v3 API's false-merge-flag fix — both the no-false-positive and still-flags-genuine-merges cases (newtext_request_adapter_test.py),get_links()'s version-metadata attribution (links_test.py), andedit_text's response shape —isPrimary/isEditedflags and position metadata (reader/tests.py).TextChunkread/save coverage parity withLegacyTextChunk(chunk_test.py, 6 new tests,LegacyTextChunk's own tests untouched):LegacyTextChunkhad comprehensive read-path coverage (verse/chapter/range/spanning refs, depth-1, nested commentary-style addressing) and an extensive save-path test (blank writes, extending beyond current extent, writing within extent, HTML sanitization, whole-chapter overwrite with blank trimming, depth-3 saves) — but that save-path test only ever exercisedLegacyTextChunk.save(), which is no longer the live save path. Added equivalent coverage for the newTextChunk, all synthetic fixtures.LegacyTextChunkeven though the production code they exercise was already migrated toTextChunk(sefaria/tests/search.py'stest_make_text_index_document,search_dependencies_test.py's book fixture, alinker_test.pyvalidation helper) — consistency, not a behavior change.resolve_default_version()guarded against an explicitpriority=None(text_request_adapter.py) —Version._normalize()can setprioritytoNoneon afloat()parse failure, not just leave it missing;getattr(v, 'priority', 0)'s default only covers the latter, somax()could raiseTypeErrorcomparingNoneto a number. Pre-existing on master (carried over unchanged when this logic was extracted into the new helper), fixed here since we were already in this function.chunk_test.py,text_test.py,schema_test.py,auto_linking_test.py,linker_test.py,links_test.py,social_image_api_test.py,search_dependencies_test.py(20 tests against the real indexing pipeline via a fake ES client) — no regressions./api/textsand/api/v3/textsfor a real ref,/api/links?with_text=1on a heavily-commented ref,/api/img-genfor en/he (viewed the rendered image).sourcesheets/views.py'sget_correct_text_from_source_obj).Merging with master
This branch was rebased against a moving
masterpartway through review. One real semantic conflict, inRef.is_text_fully_available(): master had independently added a third branch (handling refs to whole branching/structural nodes, e.g. a Sifra parsha, via_aggregate_structure_state) that didn't exist when this branch diverged. Resolved by keeping this PR'sdirection=-based migration for the segment/section-level branch (also dropping atry/except NoVersionFoundErrorthat the newTextChunkdoesn't need — confirmed no code path reaches it, since the new class returns empty text gracefully instead of raising) while preserving master's new structural-node branch intact. Verified all three branches work post-merge against real refs (a plain segment/section ref, a simple book-levelJaggedArrayNoderef, and a complex structural ref like "Pesach Haggadah").Two master-authored tests needed updates for compatibility with this PR's real-language semantics — not regressions in their own original intent, confirmed by checking both against
origin/proddirectly (prod predates this PR's changes, and both pass there):modtools_test.py::test_rename_collision_different_language_family_allowed— asserted a literal versionTitle string for a non-en/he version that this PR's auto-suffix mechanism now rewrites on save (by design, to prevent exactly the collision the test was checking couldn't happen). Updated to assert the real, suffixed behavior.text_test.py::test_index_rename_migrates_versions— its fixture insertsVersiondocs via rawdb.texts.insert_many(), bypassingVersion._normalize()and thereforeactualLanguage/languageFamilyName, whichRef.text(lang=...)now matches on instead of the legacy binarylanguagefield. Added both fields to the fixture.Three other CI failures reported during review are unrelated pre-existing breakage, confirmed independently of this branch (checked against a clean
master/prodcheckout) and intentionally left untouched:linker_editor_test.py::test_usage_index_surgical_add_remove(new, unreleased feature, never had a clean run), andsearch_dependencies_test.py'stest_hooks_noop_when_search_index_on_save_off/test_category_hook_empty_new_path_guard(a naming mismatch between a test merged 2026-08-19 and an unrelateddependencies.pyrefactor merged one week later, already broken onprod).What should be manually checked before merge
linker.v2.js-embedded pages) — spot-check that it still returns text for a few refs, including one needing merge/gap-fill across versions.topic_prompt.pyconsumesget_links(with_text=True)and calls.text()directly; worth confirming topic-prompt generation still produces correct source text.🤖 Generated with Claude Code