(function () { const MAJOR_CARDS = [ { number: 0, name: "The Fool", summary: "Open-hearted beginnings, leap of faith, and the sacred unknown.", upright: "Fresh starts, trust in the path, and inspired spontaneity.", reversed: "Recklessness, fear of risk, or resistance to a needed new beginning.", keywords: ["beginnings", "faith", "innocence", "freedom", "journey"], relations: ["Element: Air", "Hebrew Letter: Aleph"] }, { number: 1, name: "The Magus", summary: "Focused will and conscious manifestation through aligned tools.", upright: "Skill, initiative, concentration, and transforming intent into action.", reversed: "Scattered power, manipulation, or blocked self-belief.", keywords: ["will", "manifestation", "focus", "skill", "agency"], relations: ["Planet: Mercury", "Hebrew Letter: Beth"] }, { number: 2, name: "The High Priestess", summary: "Inner knowing, sacred silence, and intuitive perception.", upright: "Intuition, subtle insight, patience, and hidden wisdom.", reversed: "Disconnection from inner voice or confusion around truth.", keywords: ["intuition", "mystery", "stillness", "inner-voice", "receptivity"], relations: ["Planet: Luna", "Hebrew Letter: Gimel"] }, { number: 3, name: "The Empress", summary: "Creative abundance, nurture, and embodied growth.", upright: "Fertility, care, beauty, comfort, and creative flourishing.", reversed: "Creative drought, overgiving, or neglecting self-nourishment.", keywords: ["abundance", "creation", "nurture", "beauty", "growth"], relations: ["Planet: Venus", "Hebrew Letter: Daleth"] }, { number: 4, name: "The Emperor", summary: "Structure, authority, and stable leadership.", upright: "Order, discipline, protection, and strategic direction.", reversed: "Rigidity, domination, or weak boundaries.", keywords: ["structure", "authority", "stability", "leadership", "boundaries"], relations: ["Zodiac: Aries", "Hebrew Letter: He"] }, { number: 5, name: "The Hierophant", summary: "Tradition, spiritual instruction, and shared values.", upright: "Learning from lineage, ritual, ethics, and mentorship.", reversed: "Dogma, rebellion without grounding, or spiritual stagnation.", keywords: ["tradition", "teaching", "ritual", "ethics", "lineage"], relations: ["Zodiac: Taurus", "Hebrew Letter: Vav"] }, { number: 6, name: "The Lovers", summary: "Union, alignment, and value-centered choices.", upright: "Harmony, connection, and conscious commitment.", reversed: "Misalignment, indecision, or disconnection in relationship.", keywords: ["union", "choice", "alignment", "connection", "values"], relations: ["Zodiac: Gemini", "Hebrew Letter: Zayin"] }, { number: 7, name: "The Chariot", summary: "Directed momentum and mastery through discipline.", upright: "Determination, movement, and victory through control.", reversed: "Loss of direction, conflict of will, or stalled progress.", keywords: ["drive", "control", "momentum", "victory", "direction"], relations: ["Zodiac: Cancer", "Hebrew Letter: Cheth"] }, { number: 8, name: "Lust", summary: "Courageous life-force, passionate integrity, and heart-led power.", upright: "Vitality, confidence, and wholehearted creative expression.", reversed: "Self-doubt, depletion, or misdirected desire.", keywords: ["vitality", "passion", "courage", "magnetism", "heart-power"], relations: ["Zodiac: Leo", "Hebrew Letter: Teth"] }, { number: 9, name: "The Hermit", summary: "Inner pilgrimage, discernment, and sacred solitude.", upright: "Reflection, guidance, and deepening wisdom.", reversed: "Isolation, avoidance, or overanalysis.", keywords: ["solitude", "wisdom", "search", "reflection", "discernment"], relations: ["Zodiac: Virgo", "Hebrew Letter: Yod"] }, { number: 10, name: "Fortune", summary: "Cycles, timing, and turning points guided by greater rhythms.", upright: "Opportunity, momentum, and fated change.", reversed: "Resistance to cycles, delay, or repeating old patterns.", keywords: ["cycles", "timing", "change", "destiny", "turning-point"], relations: ["Planet: Jupiter", "Hebrew Letter: Kaph"] }, { number: 11, name: "Justice", summary: "Balance, accountability, and clear consequence.", upright: "Fairness, truth, and ethical alignment.", reversed: "Bias, denial, or avoidance of responsibility.", keywords: ["balance", "truth", "accountability", "law", "clarity"], relations: ["Zodiac: Libra", "Hebrew Letter: Lamed"] }, { number: 12, name: "The Hanged Man", summary: "Sacred pause, surrender, and transformed perspective.", upright: "Release, contemplation, and spiritual reframing.", reversed: "Stagnation, martyrdom, or refusing to let go.", keywords: ["surrender", "pause", "perspective", "suspension", "release"], relations: ["Element: Water", "Hebrew Letter: Mem"] }, { number: 13, name: "Death", summary: "Endings that clear space for rebirth and renewal.", upright: "Transformation, completion, and deep release.", reversed: "Clinging, fear of change, or prolonged transition.", keywords: ["transformation", "ending", "renewal", "release", "rebirth"], relations: ["Zodiac: Scorpio", "Hebrew Letter: Nun"] }, { number: 14, name: "Art", summary: "Alchemy, integration, and harmonizing opposites.", upright: "Balance through blending, refinement, and patience.", reversed: "Imbalance, excess, or fragmented energies.", keywords: ["alchemy", "integration", "balance", "blending", "refinement"], relations: ["Zodiac: Sagittarius", "Hebrew Letter: Samekh"] }, { number: 15, name: "The Devil", summary: "Attachment, shadow desire, and material entanglement.", upright: "Confronting bondage, ambition, and raw instinct.", reversed: "Liberation, breaking chains, or denial of shadow.", keywords: ["attachment", "shadow", "instinct", "temptation", "bondage"], relations: ["Zodiac: Capricorn", "Hebrew Letter: Ayin"] }, { number: 16, name: "The Tower", summary: "Sudden revelation that dismantles false structures.", upright: "Shock, breakthrough, and necessary collapse.", reversed: "Delayed upheaval, denial, or internal crisis.", keywords: ["upheaval", "revelation", "breakthrough", "collapse", "truth"], relations: ["Planet: Mars", "Hebrew Letter: Pe"] }, { number: 17, name: "The Star", summary: "Healing hope, guidance, and spiritual renewal.", upright: "Inspiration, serenity, and trust in the future.", reversed: "Discouragement, doubt, or loss of faith.", keywords: ["hope", "healing", "guidance", "renewal", "faith"], relations: ["Zodiac: Aquarius", "Hebrew Letter: Tzaddi"] }, { number: 18, name: "The Moon", summary: "Dream, uncertainty, and passage through the subconscious.", upright: "Intuition, mystery, and emotional depth.", reversed: "Confusion clearing, projection, or fear illusions.", keywords: ["dream", "mystery", "intuition", "subconscious", "illusion"], relations: ["Zodiac: Pisces", "Hebrew Letter: Qoph"] }, { number: 19, name: "The Sun", summary: "Radiance, confidence, and vital life affirmation.", upright: "Joy, clarity, success, and openness.", reversed: "Temporary clouding, ego glare, or delayed confidence.", keywords: ["joy", "clarity", "vitality", "success", "radiance"], relations: ["Planet: Sol", "Hebrew Letter: Resh"] }, { number: 20, name: "Aeon", summary: "Awakening call, reckoning, and soul-level renewal.", upright: "Judgment, liberation, and answering purpose.", reversed: "Self-judgment, hesitation, or resistance to calling.", keywords: ["awakening", "reckoning", "calling", "renewal", "release"], relations: ["Element: Fire", "Hebrew Letter: Shin"] }, { number: 21, name: "Universe", summary: "Completion, integration, and embodied wholeness.", upright: "Fulfillment, mastery, and successful closure.", reversed: "Incomplete cycle, loose ends, or delayed completion.", keywords: ["completion", "wholeness", "integration", "mastery", "closure"], relations: ["Planet: Saturn", "Hebrew Letter: Tav"] } ]; const SUITS = ["Cups", "Wands", "Swords", "Disks"]; const NUMBER_RANKS = ["Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"]; const COURT_RANKS = ["Knight", "Queen", "Prince", "Princess"]; const SUIT_INFO = { cups: { element: "Water", domain: "emotion, intuition, relationship", keywords: ["emotion", "intuition", "connection"] }, wands: { element: "Fire", domain: "will, creativity, drive", keywords: ["will", "drive", "inspiration"] }, swords: { element: "Air", domain: "mind, truth, communication", keywords: ["mind", "truth", "clarity"] }, disks: { element: "Earth", domain: "body, craft, material life", keywords: ["resources", "craft", "stability"] } }; const RANK_INFO = { ace: { number: 1, upright: "Seed-force, pure potential, and concentrated essence.", reversed: "Blocked potential, delay, or difficulty beginning.", keywords: ["seed", "potential", "spark"] }, two: { number: 2, upright: "Polarity seeking balance, exchange, and response.", reversed: "Imbalance, friction, or uncertain choices.", keywords: ["duality", "balance", "exchange"] }, three: { number: 3, upright: "Initial growth, expansion, and expression.", reversed: "Scattered growth or stalled development.", keywords: ["growth", "expansion", "expression"] }, four: { number: 4, upright: "Structure, boundaries, and stabilizing form.", reversed: "Rigidity, inertia, or unstable foundations.", keywords: ["structure", "stability", "foundation"] }, five: { number: 5, upright: "Challenge, disruption, and catalytic conflict.", reversed: "Lingering tension or fear of needed change.", keywords: ["challenge", "conflict", "change"] }, six: { number: 6, upright: "Rebalancing, harmony, and restorative flow.", reversed: "Partial recovery or unresolved imbalance.", keywords: ["harmony", "repair", "flow"] }, seven: { number: 7, upright: "Testing, strategy, and spiritual/mental refinement.", reversed: "Doubt, overdefense, or poor strategy.", keywords: ["test", "strategy", "refinement"] }, eight: { number: 8, upright: "Adjustment, movement, and disciplined power.", reversed: "Restriction, frustration, or scattered effort.", keywords: ["movement", "discipline", "adjustment"] }, nine: { number: 9, upright: "Intensity, culmination, and mature force.", reversed: "Excess, overload, or unresolved pressure.", keywords: ["culmination", "intensity", "maturity"] }, ten: { number: 10, upright: "Completion, overflow, and transition into a new cycle.", reversed: "Overextension, depletion, or difficulty releasing.", keywords: ["completion", "threshold", "transition"] } }; const COURT_INFO = { knight: { role: "active catalyst and questing force", elementalFace: "Fire of", upright: "Bold pursuit, direct action, and forward momentum.", reversed: "Impulsiveness, burnout, or unfocused drive.", keywords: ["action", "drive", "initiative"] }, queen: { role: "magnetic vessel and emotional intelligence", elementalFace: "Water of", upright: "Receptive mastery, mature feeling, and embodied wisdom.", reversed: "Withholding, moodiness, or passive control.", keywords: ["receptivity", "maturity", "depth"] }, prince: { role: "strategic mediator and organizing mind", elementalFace: "Air of", upright: "Insight, communication, and adaptive coordination.", reversed: "Overthinking, detachment, or mixed signals.", keywords: ["strategy", "communication", "adaptability"] }, princess: { role: "manifesting ground and practical seed-force", elementalFace: "Earth of", upright: "Practical growth, devotion, and tangible follow-through.", reversed: "Stagnation, timidity, or blocked growth.", keywords: ["manifestation", "grounding", "devotion"] } }; const MAJOR_ALIASES = { magician: "magus", strength: "lust", "wheel of fortune": "fortune", temperance: "art", judgement: "aeon", judgment: "aeon", world: "universe", adjustment: "justice" }; const MINOR_SUIT_ALIASES = { pentacles: "disks", pentacle: "disks", coins: "disks", disks: "disks" }; const MINOR_NUMERAL_ALIASES = { 1: "ace", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six", 7: "seven", 8: "eight", 9: "nine", 10: "ten" }; const MONTH_NAME_BY_NUMBER = { 1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December" }; const MONTH_ID_BY_NUMBER = { 1: "january", 2: "february", 3: "march", 4: "april", 5: "may", 6: "june", 7: "july", 8: "august", 9: "september", 10: "october", 11: "november", 12: "december" }; const COURT_DECAN_WINDOWS = { "Knight of Wands": ["scorpio-3", "sagittarius-1", "sagittarius-2"], "Queen of Disks": ["sagittarius-3", "capricorn-1", "capricorn-2"], "Prince of Swords": ["capricorn-3", "aquarius-1", "aquarius-2"], "Knight of Cups": ["aquarius-3", "pisces-1", "pisces-2"], "Queen of Wands": ["pisces-3", "aries-1", "aries-2"], "Prince of Disks": ["aries-3", "taurus-1", "taurus-2"], "Knight of Swords": ["taurus-3", "gemini-1", "gemini-2"], "Queen of Cups": ["gemini-3", "cancer-1", "cancer-2"], "Prince of Wands": ["cancer-3", "leo-1", "leo-2"], "Knight of Disks": ["leo-3", "virgo-1", "virgo-2"], "Queen of Swords": ["virgo-3", "libra-1", "libra-2"], "Prince of Cups": ["libra-3", "scorpio-1", "scorpio-2"] }; const MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const MAJOR_HEBREW_LETTER_ID_BY_CARD = { fool: "alef", magus: "bet", "high priestess": "gimel", empress: "dalet", emperor: "he", hierophant: "vav", lovers: "zayin", chariot: "het", lust: "tet", hermit: "yod", fortune: "kaf", justice: "lamed", "hanged man": "mem", death: "nun", art: "samekh", devil: "ayin", tower: "pe", star: "tsadi", moon: "qof", sun: "resh", aeon: "shin", universe: "tav" }; const HEBREW_LETTER_ALIASES = { aleph: "alef", alef: "alef", beth: "bet", bet: "bet", gimel: "gimel", daleth: "dalet", dalet: "dalet", he: "he", vav: "vav", zayin: "zayin", cheth: "het", chet: "het", het: "het", teth: "tet", tet: "tet", yod: "yod", kaph: "kaf", kaf: "kaf", lamed: "lamed", mem: "mem", nun: "nun", samekh: "samekh", ayin: "ayin", pe: "pe", tzaddi: "tsadi", tzadi: "tsadi", tsadi: "tsadi", qoph: "qof", qof: "qof", resh: "resh", shin: "shin", tav: "tav" }; function getTarotDbConfig(referenceData) { const db = referenceData?.tarotDatabase; const hasDb = db && typeof db === "object"; const majorCards = hasDb && Array.isArray(db.majorCards) && db.majorCards.length ? db.majorCards : MAJOR_CARDS; const suits = hasDb && Array.isArray(db.suits) && db.suits.length ? db.suits : SUITS; const numberRanks = hasDb && Array.isArray(db.numberRanks) && db.numberRanks.length ? db.numberRanks : NUMBER_RANKS; const courtRanks = hasDb && Array.isArray(db.courtRanks) && db.courtRanks.length ? db.courtRanks : COURT_RANKS; const suitInfo = hasDb && db.suitInfo && typeof db.suitInfo === "object" ? db.suitInfo : SUIT_INFO; const rankInfo = hasDb && db.rankInfo && typeof db.rankInfo === "object" ? db.rankInfo : RANK_INFO; const courtInfo = hasDb && db.courtInfo && typeof db.courtInfo === "object" ? db.courtInfo : COURT_INFO; const courtDecanWindows = hasDb && db.courtDecanWindows && typeof db.courtDecanWindows === "object" ? db.courtDecanWindows : COURT_DECAN_WINDOWS; return { majorCards, suits, numberRanks, courtRanks, suitInfo, rankInfo, courtInfo, courtDecanWindows }; } function normalizeCardName(value) { return String(value || "") .trim() .toLowerCase() .replace(/^the\s+/, "") .replace(/\s+/g, " "); } function canonicalCardName(value) { const normalized = normalizeCardName(value); const majorCanonical = MAJOR_ALIASES[normalized] || normalized; const withSuitAliases = majorCanonical.replace(/\bof\s+(pentacles?|coins?)\b/i, "of disks"); const numberMatch = withSuitAliases.match(/^(\d{1,2})\s+of\s+(.+)$/i); if (numberMatch) { const number = Number(numberMatch[1]); const suit = String(numberMatch[2] || "").trim(); const numberWord = MINOR_NUMERAL_ALIASES[number]; if (numberWord && suit) { return `${numberWord} of ${suit}`; } } return withSuitAliases; } function parseMonthDay(value) { const [month, day] = String(value || "").split("-").map((part) => Number(part)); if (!Number.isFinite(month) || !Number.isFinite(day)) { return null; } return { month, day }; } function monthDayToDate(monthDay, year) { const parsed = parseMonthDay(monthDay); if (!parsed) { return null; } return new Date(year, parsed.month - 1, parsed.day); } function addDays(date, days) { const next = new Date(date); next.setDate(next.getDate() + days); return next; } function formatMonthDayLabel(date) { if (!(date instanceof Date)) { return "--"; } return `${MONTH_SHORT[date.getMonth()]} ${date.getDate()}`; } function formatMonthDayToken(date) { if (!(date instanceof Date)) { return ""; } const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${month}-${day}`; } function normalizeMinorTarotCardName(value) { const normalized = canonicalCardName(value); return normalized .split(" ") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function deriveSummaryFromMeaning(meaningText, fallbackSummary) { const normalized = String(meaningText || "") .replace(/\s+/g, " ") .trim(); if (!normalized) { return fallbackSummary; } const sentenceMatch = normalized.match(/^(.+?[.!?])(?:\s|$)/); if (sentenceMatch && sentenceMatch[1]) { return sentenceMatch[1].trim(); } if (normalized.length <= 220) { return normalized; } return `${normalized.slice(0, 217).trimEnd()}…`; } function applyMeaningText(cards, referenceData) { const majorByTrumpNumber = referenceData?.tarotDatabase?.meanings?.majorByTrumpNumber; const byCardName = referenceData?.tarotDatabase?.meanings?.byCardName; if ((!majorByTrumpNumber || typeof majorByTrumpNumber !== "object") && (!byCardName || typeof byCardName !== "object")) { return cards; } return cards.map((card) => { const trumpNumber = Number(card?.number); const isMajorTrumpCard = card?.arcana === "Major" && Number.isFinite(trumpNumber); const canonicalName = canonicalCardName(card?.name); const majorMeaning = isMajorTrumpCard ? String(majorByTrumpNumber?.[trumpNumber] || "").trim() : ""; const nameMeaning = String(byCardName?.[canonicalName] || "").trim(); const selectedMeaning = majorMeaning || nameMeaning; if (!selectedMeaning) { return card; } return { ...card, meaning: selectedMeaning, summary: deriveSummaryFromMeaning(selectedMeaning, card.summary), meanings: { upright: selectedMeaning, reversed: card?.meanings?.reversed || "" } }; }); } function getSignDateBounds(sign) { const start = monthDayToDate(sign?.start, 2025); const endBase = monthDayToDate(sign?.end, 2025); if (!start || !endBase) { return null; } const wraps = endBase.getTime() < start.getTime(); const end = wraps ? monthDayToDate(sign?.end, 2026) : endBase; if (!end) { return null; } return { start, end }; } function buildDecanDateRange(sign, decanIndex) { const bounds = getSignDateBounds(sign); if (!bounds || !Number.isFinite(Number(decanIndex))) { return null; } const index = Number(decanIndex); const start = addDays(bounds.start, (index - 1) * 10); const nominalEnd = addDays(start, 9); const end = nominalEnd.getTime() > bounds.end.getTime() ? bounds.end : nominalEnd; return { start, end, startMonth: start.getMonth() + 1, endMonth: end.getMonth() + 1, startToken: formatMonthDayToken(start), endToken: formatMonthDayToken(end), label: `${formatMonthDayLabel(start)}–${formatMonthDayLabel(end)}` }; } function listMonthNumbersBetween(start, end) { if (!(start instanceof Date) || !(end instanceof Date)) { return []; } const result = []; const seen = new Set(); const cursor = new Date(start.getFullYear(), start.getMonth(), 1); const limit = new Date(end.getFullYear(), end.getMonth(), 1); while (cursor.getTime() <= limit.getTime()) { const monthNo = cursor.getMonth() + 1; if (!seen.has(monthNo)) { seen.add(monthNo); result.push(monthNo); } cursor.setMonth(cursor.getMonth() + 1); } return result; } function getSignName(sign, fallback) { return sign?.name?.en || sign?.name || sign?.id || fallback || "Unknown"; } function buildDecanMetadata(decan, sign) { if (!decan || !sign) { return null; } const index = Number(decan.index); if (!Number.isFinite(index)) { return null; } const startDegree = (index - 1) * 10; const endDegree = startDegree + 10; const dateRange = buildDecanDateRange(sign, index); return { decan, sign, index, signId: sign.id, signName: getSignName(sign, decan.signId), signSymbol: sign.symbol || "", startDegree, endDegree, dateRange, normalizedCardName: normalizeMinorTarotCardName(decan.tarotMinorArcana || "") }; } function collectCalendarMonthRelationsFromDecan(targetKey, relationMap, decanMeta) { const dateRange = decanMeta?.dateRange; if (!dateRange?.start || !dateRange?.end) { return; } const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end); monthNumbers.forEach((monthNo) => { const monthId = MONTH_ID_BY_NUMBER[monthNo]; const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`; if (!monthId) { return; } pushMapValue( relationMap, targetKey, createRelation( "calendarMonth", `${monthId}-${decanMeta.signId}-${decanMeta.index}`, `Calendar month: ${monthName} (${decanMeta.signName} decan ${decanMeta.index})`, { monthId, name: monthName, monthOrder: monthNo, signId: decanMeta.signId, signName: decanMeta.signName, decanIndex: decanMeta.index, dateRange: dateRange.label } ) ); }); } function normalizeHebrewKey(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z]/g, ""); } function buildHebrewLetterLookup(magickDataset) { const letters = magickDataset?.grouped?.hebrewLetters; const lookup = new Map(); if (!letters || typeof letters !== "object") { return lookup; } Object.entries(letters).forEach(([letterId, entry]) => { const idKey = normalizeHebrewKey(letterId); const canonicalKey = HEBREW_LETTER_ALIASES[idKey] || idKey; if (canonicalKey && !lookup.has(canonicalKey)) { lookup.set(canonicalKey, entry); } const nameKey = normalizeHebrewKey(entry?.letter?.name); const canonicalNameKey = HEBREW_LETTER_ALIASES[nameKey] || nameKey; if (canonicalNameKey && !lookup.has(canonicalNameKey)) { lookup.set(canonicalNameKey, entry); } const entryIdKey = normalizeHebrewKey(entry?.id); const canonicalEntryIdKey = HEBREW_LETTER_ALIASES[entryIdKey] || entryIdKey; if (canonicalEntryIdKey && !lookup.has(canonicalEntryIdKey)) { lookup.set(canonicalEntryIdKey, entry); } }); return lookup; } function normalizeRelationId(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || "unknown"; } function createRelation(type, id, label, data = null) { return { type, id: normalizeRelationId(id), label: String(label || "").trim(), data }; } function relationSignature(value) { if (!value || typeof value !== "object") { return String(value || ""); } return `${value.type || "text"}|${value.id || ""}|${value.label || ""}`; } function parseLegacyRelation(text) { const raw = String(text || "").trim(); const match = raw.match(/^([^:]+):\s*(.+)$/); if (!match) { return createRelation("note", raw, raw, { value: raw }); } const key = normalizeRelationId(match[1]); const value = String(match[2] || "").trim(); if (key === "element") { return createRelation("element", value, `Element: ${value}`, { name: value }); } if (key === "planet") { return createRelation("planet", value, `Planet: ${value}`, { name: value }); } if (key === "zodiac") { return createRelation("zodiac", value, `Zodiac: ${value}`, { name: value }); } if (key === "suit-domain") { return createRelation("suitDomain", value, `Suit domain: ${value}`, { value }); } if (key === "numerology") { const numeric = Number(value); return createRelation("numerology", value, `Numerology: ${value}`, { value: Number.isFinite(numeric) ? numeric : value }); } if (key === "court-role") { return createRelation("courtRole", value, `Court role: ${value}`, { value }); } if (key === "hebrew-letter") { const normalized = normalizeHebrewKey(value); const canonical = HEBREW_LETTER_ALIASES[normalized] || normalized; return createRelation("hebrewLetter", canonical, `Hebrew Letter: ${value}`, { requestedName: value }); } return createRelation(key || "relation", value, raw, { value }); } function buildHebrewLetterRelation(hebrewLetterId, hebrewLookup) { if (!hebrewLetterId || !hebrewLookup) { return null; } const normalizedId = normalizeHebrewKey(hebrewLetterId); const canonicalId = HEBREW_LETTER_ALIASES[normalizedId] || normalizedId; const entry = hebrewLookup.get(canonicalId); if (!entry) { return createRelation("hebrewLetter", canonicalId, `Hebrew Letter: ${hebrewLetterId}`, null); } const glyph = entry?.letter?.he || ""; const name = entry?.letter?.name || hebrewLetterId; const latin = entry?.letter?.latin || ""; const index = Number.isFinite(entry?.index) ? entry.index : null; const value = Number.isFinite(entry?.value) ? entry.value : null; const meaning = entry?.meaning?.en || ""; const indexText = index !== null ? index : "?"; const valueText = value !== null ? value : "?"; const meaningText = meaning ? ` · ${meaning}` : ""; return createRelation( "hebrewLetter", entry?.id || canonicalId, `Hebrew Letter: ${glyph} ${name} (${latin}) (index ${indexText}, value ${valueText})${meaningText}`.trim(), { id: entry?.id || canonicalId, glyph, name, latin, index, value, meaning } ); } function pushMapValue(map, key, value) { if (!key || !value) { return; } if (!map.has(key)) { map.set(key, []); } const existing = map.get(key); const signature = relationSignature(value); const duplicate = existing.some((entry) => relationSignature(entry) === signature); if (!duplicate) { existing.push(value); } } function buildMajorDynamicRelations(referenceData) { const relationMap = new Map(); const planets = referenceData?.planets && typeof referenceData.planets === "object" ? Object.values(referenceData.planets) : []; planets.forEach((planet) => { const cardName = planet?.tarot?.majorArcana; if (!cardName) { return; } const relation = createRelation( "planetCorrespondence", planet?.id || planet?.name || cardName, `Planet correspondence: ${planet.symbol || ""} ${planet.name || ""}`.trim(), { planetId: planet?.id || null, symbol: planet?.symbol || "", name: planet?.name || "" } ); pushMapValue(relationMap, canonicalCardName(cardName), relation); }); const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : []; signs.forEach((sign) => { const cardName = sign?.tarot?.majorArcana; if (!cardName) { return; } const relation = createRelation( "zodiacCorrespondence", sign?.id || sign?.name || cardName, `Zodiac correspondence: ${sign.symbol || ""} ${sign.name || ""}`.trim(), { signId: sign?.id || null, symbol: sign?.symbol || "", name: sign?.name || "" } ); pushMapValue(relationMap, canonicalCardName(cardName), relation); }); return relationMap; } function buildMinorDecanRelations(referenceData) { const relationMap = new Map(); const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : []; const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign])); const planets = referenceData?.planets || {}; if (!referenceData?.decansBySign || typeof referenceData.decansBySign !== "object") { return relationMap; } Object.entries(referenceData.decansBySign).forEach(([signId, decans]) => { const sign = signById[signId]; if (!Array.isArray(decans) || !sign) { return; } decans.forEach((decan) => { const cardName = decan?.tarotMinorArcana; if (!cardName) { return; } const decanMeta = buildDecanMetadata(decan, sign); if (!decanMeta) { return; } const { startDegree, endDegree, dateRange, signId, signName, signSymbol, index } = decanMeta; const ruler = planets[decan.rulerPlanetId] || null; const cardKey = canonicalCardName(cardName); pushMapValue( relationMap, cardKey, createRelation( "zodiac", signId, `Zodiac: ${sign.symbol || ""} ${signName}`.trim(), { signId, signName, symbol: sign.symbol || "" } ) ); pushMapValue( relationMap, cardKey, createRelation( "decan", `${signId}-${index}`, `Decan ${decan.index}: ${sign.symbol || ""} ${signName} (${startDegree}°–${endDegree}°)${dateRange ? ` · ${dateRange.label}` : ""}`.trim(), { signId, signName, signSymbol, index, startDegree, endDegree, dateStart: dateRange?.startToken || null, dateEnd: dateRange?.endToken || null, dateRange: dateRange?.label || null } ) ); collectCalendarMonthRelationsFromDecan(cardKey, relationMap, decanMeta); if (ruler) { pushMapValue( relationMap, cardKey, createRelation( "decanRuler", `${signId}-${index}-${decan.rulerPlanetId}`, `Decan ruler: ${ruler.symbol || ""} ${ruler.name || decan.rulerPlanetId}`.trim(), { signId, decanIndex: index, planetId: decan.rulerPlanetId, symbol: ruler.symbol || "", name: ruler.name || decan.rulerPlanetId } ) ); } }); }); return relationMap; } function buildMajorCards(referenceData, magickDataset) { const tarotDb = getTarotDbConfig(referenceData); const dynamicRelations = buildMajorDynamicRelations(referenceData); const hebrewLookup = buildHebrewLetterLookup(magickDataset); return tarotDb.majorCards.map((card) => { const canonicalName = canonicalCardName(card.name); const dynamic = dynamicRelations.get(canonicalName) || []; const hebrewLetterId = MAJOR_HEBREW_LETTER_ID_BY_CARD[canonicalName] || null; const hebrewLetterRelation = buildHebrewLetterRelation(hebrewLetterId, hebrewLookup); const staticRelations = (card.relations || []) .map((relation) => parseLegacyRelation(relation)) .filter((relation) => relation.type !== "hebrewLetter" && relation.type !== "zodiac" && relation.type !== "planet"); return { arcana: "Major", name: card.name, number: card.number, suit: null, rank: null, hebrewLetterId, hebrewLetter: hebrewLetterRelation?.data || null, summary: card.summary, meanings: { upright: card.upright, reversed: card.reversed }, keywords: [...card.keywords], relations: [ ...staticRelations, ...(hebrewLetterRelation ? [hebrewLetterRelation] : []), ...dynamic ] }; }); } function buildNumberMinorCards(referenceData) { const tarotDb = getTarotDbConfig(referenceData); const decanRelations = buildMinorDecanRelations(referenceData); const cards = []; tarotDb.suits.forEach((suit) => { const suitKey = suit.toLowerCase(); const suitInfo = tarotDb.suitInfo[suitKey]; if (!suitInfo) { return; } tarotDb.numberRanks.forEach((rank) => { const rankKey = rank.toLowerCase(); const rankInfo = tarotDb.rankInfo[rankKey]; if (!rankInfo) { return; } const cardName = `${rank} of ${suit}`; const dynamicRelations = decanRelations.get(canonicalCardName(cardName)) || []; cards.push({ arcana: "Minor", name: cardName, number: null, suit, rank, summary: `${rank} energy expressed through ${suitInfo.domain}.`, meanings: { upright: `${rankInfo.upright} In ${suit}, this emphasizes ${suitInfo.domain}.`, reversed: `${rankInfo.reversed} In ${suit}, this may distort ${suitInfo.domain}.` }, keywords: [...rankInfo.keywords, ...suitInfo.keywords], relations: [ createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, { name: suitInfo.element }), createRelation("suitDomain", `${suitKey}-${rankKey}`, `Suit domain: ${suitInfo.domain}`, { suit: suit, rank, domain: suitInfo.domain }), createRelation("numerology", rankInfo.number, `Numerology: ${rankInfo.number}`, { value: rankInfo.number }), ...dynamicRelations ] }); }); }); return cards; } function buildCourtMinorCards(referenceData) { const tarotDb = getTarotDbConfig(referenceData); const cards = []; const signs = Array.isArray(referenceData?.signs) ? referenceData.signs : []; const signById = Object.fromEntries(signs.map((sign) => [sign.id, sign])); const decanById = new Map(); const decansBySign = referenceData?.decansBySign || {}; Object.entries(decansBySign).forEach(([signId, decans]) => { const sign = signById[signId]; if (!sign || !Array.isArray(decans)) { return; } decans.forEach((decan) => { if (!decan?.id) { return; } const meta = buildDecanMetadata(decan, sign); if (meta) { decanById.set(decan.id, meta); } }); }); tarotDb.suits.forEach((suit) => { const suitKey = suit.toLowerCase(); const suitInfo = tarotDb.suitInfo[suitKey]; if (!suitInfo) { return; } tarotDb.courtRanks.forEach((rank) => { const rankKey = rank.toLowerCase(); const courtInfo = tarotDb.courtInfo[rankKey]; if (!courtInfo) { return; } const cardName = `${rank} of ${suit}`; const windowDecanIds = tarotDb.courtDecanWindows[cardName] || []; const windowDecans = windowDecanIds .map((decanId) => decanById.get(decanId) || null) .filter(Boolean); const dynamicRelations = []; const monthKeys = new Set(); windowDecans.forEach((meta) => { dynamicRelations.push( createRelation( "decan", `${meta.signId}-${meta.index}-${rankKey}-${suitKey}`, `Decan ${meta.index}: ${meta.signSymbol} ${meta.signName} (${meta.startDegree}°–${meta.endDegree}°)${meta.dateRange ? ` · ${meta.dateRange.label}` : ""}`.trim(), { signId: meta.signId, signName: meta.signName, signSymbol: meta.signSymbol, index: meta.index, startDegree: meta.startDegree, endDegree: meta.endDegree, dateStart: meta.dateRange?.startToken || null, dateEnd: meta.dateRange?.endToken || null, dateRange: meta.dateRange?.label || null } ) ); const dateRange = meta.dateRange; if (dateRange?.start && dateRange?.end) { const monthNumbers = listMonthNumbersBetween(dateRange.start, dateRange.end); monthNumbers.forEach((monthNo) => { const monthId = MONTH_ID_BY_NUMBER[monthNo]; const monthName = MONTH_NAME_BY_NUMBER[monthNo] || `Month ${monthNo}`; const monthKey = `${monthId}:${meta.signId}:${meta.index}`; if (!monthId || monthKeys.has(monthKey)) { return; } monthKeys.add(monthKey); dynamicRelations.push( createRelation( "calendarMonth", `${monthId}-${meta.signId}-${meta.index}-${rankKey}-${suitKey}`, `Calendar month: ${monthName} (${meta.signName} decan ${meta.index})`, { monthId, name: monthName, monthOrder: monthNo, signId: meta.signId, signName: meta.signName, decanIndex: meta.index, dateRange: meta.dateRange?.label || null } ) ); }); } }); if (windowDecans.length) { const firstRange = windowDecans[0].dateRange; const lastRange = windowDecans[windowDecans.length - 1].dateRange; const windowLabel = firstRange && lastRange ? `${formatMonthDayLabel(firstRange.start)}–${formatMonthDayLabel(lastRange.end)}` : "--"; dynamicRelations.unshift( createRelation( "courtDateWindow", `${rankKey}-${suitKey}`, `Court date window: ${windowLabel}`, { dateStart: firstRange?.startToken || null, dateEnd: lastRange?.endToken || null, dateRange: windowLabel, decanIds: windowDecanIds } ) ); } cards.push({ arcana: "Minor", name: cardName, number: null, suit, rank, summary: `${rank} as ${courtInfo.role} within ${suitInfo.domain}.`, meanings: { upright: `${courtInfo.upright} In ${suit}, this guides ${suitInfo.domain}.`, reversed: `${courtInfo.reversed} In ${suit}, this complicates ${suitInfo.domain}.` }, keywords: [...courtInfo.keywords, ...suitInfo.keywords], relations: [ createRelation("element", suitInfo.element, `Element: ${suitInfo.element}`, { name: suitInfo.element }), createRelation( "elementalFace", `${rankKey}-${suitKey}`, `${courtInfo.elementalFace} ${suitInfo.element}`, { rank, suit, elementalFace: courtInfo.elementalFace, element: suitInfo.element } ), createRelation("courtRole", rankKey, `Court role: ${courtInfo.role}`, { rank, role: courtInfo.role }), ...dynamicRelations ] }); }); }); return cards; } function buildTarotDatabase(referenceData, magickDataset = null) { const cards = [ ...buildMajorCards(referenceData, magickDataset), ...buildNumberMinorCards(referenceData), ...buildCourtMinorCards(referenceData) ]; return applyMeaningText(cards, referenceData); } window.TarotCardDatabase = { buildTarotDatabase }; })();