refraction almost completed

This commit is contained in:
2026-03-07 13:38:13 -08:00
parent 3c07a13547
commit d44483de5e
37 changed files with 8506 additions and 7145 deletions

View File

@@ -459,52 +459,49 @@
tav: "tav"
};
const createTarotDatabaseHelpers = window.TarotDatabaseBuilders?.createTarotDatabaseHelpers;
const createTarotDatabaseAssembly = window.TarotDatabaseAssembly?.createTarotDatabaseAssembly;
if (typeof createTarotDatabaseHelpers !== "function" || typeof createTarotDatabaseAssembly !== "function") {
throw new Error("TarotDatabaseBuilders and TarotDatabaseAssembly modules must load before tarot-database.js");
}
const tarotDatabaseBuilders = createTarotDatabaseHelpers({
majorCards: MAJOR_CARDS,
suits: SUITS,
numberRanks: NUMBER_RANKS,
courtRanks: COURT_RANKS,
suitInfo: SUIT_INFO,
rankInfo: RANK_INFO,
courtInfo: COURT_INFO,
courtDecanWindows: COURT_DECAN_WINDOWS,
majorAliases: MAJOR_ALIASES,
minorNumeralAliases: MINOR_NUMERAL_ALIASES,
monthNameByNumber: MONTH_NAME_BY_NUMBER,
monthIdByNumber: MONTH_ID_BY_NUMBER,
monthShort: MONTH_SHORT,
hebrewLetterAliases: HEBREW_LETTER_ALIASES
});
const tarotDatabaseAssembly = createTarotDatabaseAssembly({
getTarotDbConfig: tarotDatabaseBuilders.getTarotDbConfig,
canonicalCardName: tarotDatabaseBuilders.canonicalCardName,
formatMonthDayLabel: tarotDatabaseBuilders.formatMonthDayLabel,
applyMeaningText: tarotDatabaseBuilders.applyMeaningText,
buildDecanMetadata: tarotDatabaseBuilders.buildDecanMetadata,
listMonthNumbersBetween: tarotDatabaseBuilders.listMonthNumbersBetween,
buildHebrewLetterLookup: tarotDatabaseBuilders.buildHebrewLetterLookup,
createRelation: tarotDatabaseBuilders.createRelation,
parseLegacyRelation: tarotDatabaseBuilders.parseLegacyRelation,
buildHebrewLetterRelation: tarotDatabaseBuilders.buildHebrewLetterRelation,
buildMajorDynamicRelations: tarotDatabaseBuilders.buildMajorDynamicRelations,
buildMinorDecanRelations: tarotDatabaseBuilders.buildMinorDecanRelations,
monthNameByNumber: MONTH_NAME_BY_NUMBER,
monthIdByNumber: MONTH_ID_BY_NUMBER,
majorHebrewLetterIdByCard: MAJOR_HEBREW_LETTER_ID_BY_CARD
});
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
};
return tarotDatabaseBuilders.getTarotDbConfig(referenceData);
}
function normalizeCardName(value) {
@@ -516,290 +513,27 @@
}
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;
return tarotDatabaseBuilders.canonicalCardName(value);
}
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()}`;
return tarotDatabaseBuilders.formatMonthDayLabel(date);
}
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)}`
};
return tarotDatabaseBuilders.applyMeaningText(cards, referenceData);
}
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";
return tarotDatabaseBuilders.listMonthNumbersBetween(start, end);
}
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, "");
return tarotDatabaseBuilders.buildDecanMetadata(decan, sign);
}
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;
return tarotDatabaseBuilders.buildHebrewLetterLookup(magickDataset);
}
function normalizeRelationId(value) {
@@ -811,528 +545,39 @@
}
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 || ""}`;
return tarotDatabaseBuilders.createRelation(type, id, label, data);
}
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 });
return tarotDatabaseBuilders.parseLegacyRelation(text);
}
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);
}
return tarotDatabaseBuilders.buildHebrewLetterRelation(hebrewLetterId, hebrewLookup);
}
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;
return tarotDatabaseBuilders.buildMajorDynamicRelations(referenceData);
}
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;
return tarotDatabaseBuilders.buildMinorDecanRelations(referenceData);
}
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
]
};
});
return tarotDatabaseAssembly.buildMajorCards(referenceData, magickDataset);
}
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;
return tarotDatabaseAssembly.buildNumberMinorCards(referenceData);
}
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;
return tarotDatabaseAssembly.buildCourtMinorCards(referenceData);
}
function buildTarotDatabase(referenceData, magickDataset = null) {
const cards = [
...buildMajorCards(referenceData, magickDataset),
...buildNumberMinorCards(referenceData),
...buildCourtMinorCards(referenceData)
];
return applyMeaningText(cards, referenceData);
return tarotDatabaseAssembly.buildTarotDatabase(referenceData, magickDataset);
}
window.TarotCardDatabase = {