Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ internal class SefariaBookPayloadReader(
val collectiveTitleEn = (schemaJson["collective_title"] as? JsonObject)
?.get("en")?.stringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
val titleAliasKeys = extractTitleAliasKeys(schemaJson, schemaObj)
val isReferenceWork = schemaJson["categories"]?.jsonArray
?.firstOrNull()?.jsonPrimitive?.contentOrNull == "Reference"

BookPayload(
heTitle = hebrewTitle,
Expand All @@ -150,6 +152,7 @@ internal class SefariaBookPayloadReader(
baseTextTitleKeys = baseTextTitleKeys,
collectiveTitleEn = collectiveTitleEn,
titleAliasKeys = titleAliasKeys,
isReferenceWork = isReferenceWork,
)
}.onFailure { e ->
logger.w(e) { "Failed to prepare book from $textPath" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ class SefariaDirectImporter(
// Resolved in a second pass; left empty for now.
baseTextBookIds = emptySet(),
collectiveTitleEn = payload.collectiveTitleEn,
isReferenceWork = payload.isReferenceWork,
)
if (payload.baseTextTitleKeys.isNotEmpty()) {
pendingBaseTextKeysByBookId[bookId] = payload.baseTextTitleKeys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ internal data class BookMeta(
// "Rashi on Exodus"…). Used by the density chain to aggregate per-collective
// signal so volume-level noise doesn't tip the per-pair ratio.
val collectiveTitleEn: String? = null,
// Sefaria `Reference` work (dictionary, lexicon…). It explains words of other
// texts without being their base or their commentary, so a link between a
// reference work and a non-reference book is never oriented.
val isReferenceWork: Boolean = false,
)

internal data class BookPayload(
Expand All @@ -65,6 +69,8 @@ internal data class BookPayload(
// so that title-pattern base parsing ("X on Y") can resolve "Y" to a bookId
// when "Y" is a Sefaria-recognised alias (e.g. "Avot" → Pirkei Avot).
val titleAliasKeys: List<String> = emptyList(),
// Top-level Sefaria category is `Reference` (dictionaries, lexicons, bibliographies).
val isReferenceWork: Boolean = false,
)

internal data class RefEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ internal fun resolveDirectionalConnectionTypesForMeta(
return baseType to baseType
}

// A reference work (e.g. Otzar La'azei Rashi, a dictionary of Rashi's Old French
// glosses) is neither the base nor a commentary of the texts it explains. Sefaria
// still types those rows `commentary`, and since a dictionary has no `dependence`
// the asymmetry below would make it Rashi's base (Zayit issue #503). Links inside
// the reference category (Hafla'ah she-ba'Arakhin on Sefer HeArukh) stay oriented.
if (sourceMeta.isReferenceWork != targetMeta.isReferenceWork) {
return ConnectionType.REFERENCE to ConnectionType.REFERENCE
}

// (1) Strongest signal: explicit base_text_titles declaration.
val targetDependsOnSource = sourceBookId in targetMeta.baseTextBookIds
val sourceDependsOnTarget = targetBookId in sourceMeta.baseTextBookIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,55 @@ class SefariaLinksImporterTest {
assertEquals(ConnectionType.COMMENTARY, reverse)
}

// Zayit #503: Sefaria types Otzar La'azei Rashi → Rashi on X rows `commentary`.
// The dictionary has no dependence, so it must not become Rashi's base.
@Test
fun referenceWorkIsNeverOrientedAgainstNonReferenceBook() {
val dictionary = BookMeta(isBaseBook = false, categoryLevel = 1, priorityRank = null, isReferenceWork = true)
val rashi = BookMeta(
isBaseBook = false,
categoryLevel = 2,
priorityRank = null,
dependence = Dependence.COMMENTARY,
)

val (forward, reverse) = resolveDirectionalConnectionTypesForMeta(
baseType = ConnectionType.COMMENTARY,
sourceBookId = 10L,
targetBookId = 20L,
sourceMeta = dictionary,
targetMeta = rashi
)

assertEquals(ConnectionType.REFERENCE, forward)
assertEquals(ConnectionType.REFERENCE, reverse)
}

// A commentary on a dictionary (Hafla'ah she-ba'Arakhin on Sefer HeArukh) stays oriented.
@Test
fun commentaryInsideReferenceCategoryKeepsOrientation() {
val arukh = BookMeta(isBaseBook = false, categoryLevel = 1, priorityRank = null, isReferenceWork = true)
val haflaah = BookMeta(
isBaseBook = false,
categoryLevel = 1,
priorityRank = null,
dependence = Dependence.COMMENTARY,
baseTextBookIds = setOf(10L),
isReferenceWork = true,
)

val (forward, reverse) = resolveDirectionalConnectionTypesForMeta(
baseType = ConnectionType.COMMENTARY,
sourceBookId = 10L,
targetBookId = 20L,
sourceMeta = arukh,
targetMeta = haflaah
)

assertEquals(ConnectionType.COMMENTARY, forward)
assertEquals(ConnectionType.SOURCE, reverse)
}

// When base_text_titles couldn't be resolved (e.g. the base book is not in
// our DB), fall back to schema dependence flag.
@Test
Expand Down
Loading