Files
TaroTime/app/tarot-database.js
2026-03-07 01:09:00 -08:00

1342 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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
};
})();