feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
+556 -3
View File
@@ -1,3 +1,83 @@
import 'culture_branch.dart';
class SymbolLayer {
final String system;
final String title;
final String law;
const SymbolLayer({
required this.system,
required this.title,
required this.law,
});
factory SymbolLayer.fromMap(Map<String, dynamic> map) {
return SymbolLayer(
system: map['system'] as String? ?? '',
title: map['title'] as String? ?? '',
law: map['law'] as String? ?? '',
);
}
Map<String, dynamic> toMap() => {
'system': system,
'title': title,
'law': law,
};
}
class SymbolSource {
final String system;
final String ref;
final double weight;
const SymbolSource({
required this.system,
required this.ref,
required this.weight,
});
factory SymbolSource.fromMap(Map<String, dynamic> map) {
return SymbolSource(
system: map['system'] as String? ?? '',
ref: map['ref'] as String? ?? '',
weight: (map['weight'] as num?)?.toDouble() ?? 1.0,
);
}
Map<String, dynamic> toMap() => {
'system': system,
'ref': ref,
'weight': weight,
};
}
class SymbolGrammar {
final String operator;
final String valence;
final String modifies;
const SymbolGrammar({
this.operator = 'substance',
this.valence = 'liminal',
this.modifies = '',
});
factory SymbolGrammar.fromMap(Map<String, dynamic> map) {
return SymbolGrammar(
operator: map['operator'] as String? ?? 'substance',
valence: map['valence'] as String? ?? 'liminal',
modifies: map['modifies'] as String? ?? '',
);
}
Map<String, dynamic> toMap() => {
'operator': operator,
'valence': valence,
'modifies': modifies,
};
}
class WordItem {
final int id; // Base index 0..999
final int sessionNumber; // Dynamic randomized session number 0..999
@@ -6,6 +86,27 @@ class WordItem {
final double frequency;
final String category;
final String description;
final String emoji;
// Enriched Oracle Knowledge Base fields
final String family;
final String operator;
final String valence;
final List<String> jurisdictions;
final String lookup; // 'A', 'B', 'C'
final bool hasCanon;
final String core;
final String oracle;
final List<SymbolLayer> layers;
final String shadow;
final String asks;
final String step;
final List<String> binds;
final List<SymbolSource> sources;
final SymbolGrammar? grammar;
final bool isUserEdited;
final Map<String, String> cultures;
final Map<String, String> languages;
const WordItem({
required this.id,
@@ -15,6 +116,25 @@ class WordItem {
required this.frequency,
this.category = '',
this.description = '',
this.emoji = '',
this.family = '',
this.operator = 'substance',
this.valence = 'liminal',
this.jurisdictions = const [],
this.lookup = 'C',
this.hasCanon = false,
this.core = '',
this.oracle = '',
this.layers = const [],
this.shadow = '',
this.asks = '',
this.step = '',
this.binds = const [],
this.sources = const [],
this.grammar,
this.isUserEdited = false,
this.cultures = const {},
this.languages = const {},
});
WordItem copyWith({
@@ -25,6 +145,25 @@ class WordItem {
double? frequency,
String? category,
String? description,
String? emoji,
String? family,
String? operator,
String? valence,
List<String>? jurisdictions,
String? lookup,
bool? hasCanon,
String? core,
String? oracle,
List<SymbolLayer>? layers,
String? shadow,
String? asks,
String? step,
List<String>? binds,
List<SymbolSource>? sources,
SymbolGrammar? grammar,
bool? isUserEdited,
Map<String, String>? cultures,
Map<String, String>? languages,
}) {
return WordItem(
id: id ?? this.id,
@@ -34,9 +173,110 @@ class WordItem {
frequency: frequency ?? this.frequency,
category: category ?? this.category,
description: description ?? this.description,
emoji: emoji ?? this.emoji,
family: family ?? this.family,
operator: operator ?? this.operator,
valence: valence ?? this.valence,
jurisdictions: jurisdictions ?? this.jurisdictions,
lookup: lookup ?? this.lookup,
hasCanon: hasCanon ?? this.hasCanon,
core: core ?? this.core,
oracle: oracle ?? this.oracle,
layers: layers ?? this.layers,
shadow: shadow ?? this.shadow,
asks: asks ?? this.asks,
step: step ?? this.step,
binds: binds ?? this.binds,
sources: sources ?? this.sources,
grammar: grammar ?? this.grammar,
isUserEdited: isUserEdited ?? this.isUserEdited,
cultures: cultures ?? this.cultures,
languages: languages ?? this.languages,
);
}
/// Enrich existing WordItem with data from oracle_kb.json entry
WordItem enrichWithKb(Map<String, dynamic> kb) {
final rawLayers = kb['layers'] as List<dynamic>? ?? [];
final parsedLayers = rawLayers
.whereType<Map<String, dynamic>>()
.map((m) => SymbolLayer.fromMap(m))
.toList();
final rawSourcesList = kb['sources'] is List ? (kb['sources'] as List) : [];
final parsedSources = rawSourcesList
.whereType<Map<String, dynamic>>()
.map((m) => SymbolSource.fromMap(m))
.toList();
final rawJurisdictions = kb['jurisdictions'] is List
? (kb['jurisdictions'] as List).map((e) => e.toString()).toList()
: <String>[];
final rawBinds = kb['binds'] is List
? (kb['binds'] as List).map((e) => e.toString()).toList()
: (kb['binds'] is String ? [kb['binds'].toString()] : <String>[]);
final rawGrammar = kb['grammar'] as Map<String, dynamic>?;
final rawKbCultures = kb['cultures'] as Map<String, dynamic>?;
final parsedKbCultures = rawKbCultures != null
? rawKbCultures.map((k, v) => MapEntry(k.toString(), v.toString()))
: <String, String>{};
final rawKbLanguages = kb['languages'] as Map<String, dynamic>?;
final parsedKbLanguages = rawKbLanguages != null
? rawKbLanguages.map((k, v) => MapEntry(k.toString(), v.toString()))
: <String, String>{};
final kbCore = (kb['core'] as String? ?? '').trim();
final kbOracle = (kb['oracle'] as String? ?? '').trim();
final kbHasCanon = (kb['has_canon'] as bool? ?? false) || (kbCore.isNotEmpty && kbOracle.isNotEmpty);
return copyWith(
partOfSpeech: partOfSpeech.isNotEmpty ? partOfSpeech : ((kb['pos'] as String?) ?? partOfSpeech),
category: (kb['category'] as String?)?.isNotEmpty == true ? kb['category'] as String : category,
family: kb['family'] as String? ?? family,
operator: kb['operator'] as String? ?? operator,
valence: kb['valence'] as String? ?? valence,
jurisdictions: rawJurisdictions.isNotEmpty ? rawJurisdictions : jurisdictions,
lookup: kb['lookup'] as String? ?? lookup,
hasCanon: kbHasCanon,
core: kbCore,
oracle: kbOracle,
layers: parsedLayers,
shadow: kb['shadow'] as String? ?? shadow,
asks: kb['asks'] as String? ?? asks,
step: kb['step'] as String? ?? step,
binds: rawBinds,
sources: parsedSources,
emoji: (kb['emoji'] as String?)?.isNotEmpty == true ? kb['emoji'] as String : emoji,
grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : grammar,
cultures: parsedKbCultures.isNotEmpty ? parsedKbCultures : cultures,
languages: parsedKbLanguages.isNotEmpty ? parsedKbLanguages : languages,
);
}
/// Returns description stripped of raw metadata bracket tags like [Таро: ...]
String get cleanDescription {
final clean = description.replaceAll(RegExp(r'\[[^\]]+\]\s*'), '').trim();
return clean.isNotEmpty ? clean : description;
}
/// Культурные ветви знака: явная редакция (cultures) приоритетна,
/// иначе — автоматическое разложение толкования по традициям.
Map<String, String> get resolvedCultures {
if (cultures.isNotEmpty) return cultures;
return extractCulturalBranches('$oracle\n\n$shadow\n\n$cleanDescription');
}
/// Primary poetic/canonical essence: core if available, otherwise clean description
String get displayMeaning {
if (core.isNotEmpty) return core;
if (cleanDescription.isNotEmpty) return cleanDescription;
return description;
}
String get formattedSessionNumber => sessionNumber.toString().padLeft(3, '0');
String get formattedId => formattedSessionNumber;
@@ -49,21 +289,334 @@ class WordItem {
'frequency': frequency,
'category': category,
'description': description,
'emoji': emoji,
'family': family,
'operator': operator,
'valence': valence,
'jurisdictions': jurisdictions,
'lookup': lookup,
'hasCanon': hasCanon,
'core': core,
'oracle': oracle,
'layers': layers.map((l) => l.toMap()).toList(),
'shadow': shadow,
'asks': asks,
'step': step,
'binds': binds,
'sources': sources.map((s) => s.toMap()).toList(),
'grammar': grammar?.toMap(),
'cultures': cultures,
'languages': languages,
};
}
factory WordItem.fromMap(Map<String, dynamic> map) {
final rawLayers = map['layers'] as List<dynamic>? ?? [];
final parsedLayers = rawLayers
.whereType<Map<String, dynamic>>()
.map((m) => SymbolLayer.fromMap(m))
.toList();
final rawSources = map['sources'] as List<dynamic>? ?? [];
final parsedSources = rawSources
.whereType<Map<String, dynamic>>()
.map((m) => SymbolSource.fromMap(m))
.toList();
final rawJurisdictions = (map['jurisdictions'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList();
final rawBinds = (map['binds'] as List<dynamic>? ?? [])
.map((e) => e.toString())
.toList();
final rawGrammar = map['grammar'] as Map<String, dynamic>?;
final rawCultures = map['cultures'] as Map<String, dynamic>?;
final parsedCultures = rawCultures != null
? rawCultures.map((k, v) => MapEntry(k.toString(), v.toString()))
: <String, String>{};
final rawLanguages = map['languages'] as Map<String, dynamic>?;
final parsedLanguages = rawLanguages != null
? rawLanguages.map((k, v) => MapEntry(k.toString(), v.toString()))
: <String, String>{};
return WordItem(
id: map['id'] as int,
sessionNumber: map['sessionNumber'] as int? ?? (map['id'] as int),
word: map['word'] as String,
partOfSpeech: map['partOfSpeech'] as String,
frequency: (map['frequency'] as num).toDouble(),
partOfSpeech: map['partOfSpeech'] as String? ?? '',
frequency: (map['frequency'] as num?)?.toDouble() ?? 50.0,
category: map['category'] as String? ?? '',
description: map['description'] as String? ?? '',
emoji: map['emoji'] as String? ?? '',
family: map['family'] as String? ?? '',
operator: map['operator'] as String? ?? 'substance',
valence: map['valence'] as String? ?? 'liminal',
jurisdictions: rawJurisdictions,
lookup: map['lookup'] as String? ?? 'C',
hasCanon: map['hasCanon'] as bool? ?? false,
core: map['core'] as String? ?? '',
oracle: map['oracle'] as String? ?? '',
layers: parsedLayers,
shadow: map['shadow'] as String? ?? '',
asks: map['asks'] as String? ?? '',
step: map['step'] as String? ?? '',
binds: rawBinds,
sources: parsedSources,
grammar: rawGrammar != null ? SymbolGrammar.fromMap(rawGrammar) : null,
isUserEdited: map['isUserEdited'] as bool? ?? false,
cultures: parsedCultures,
languages: parsedLanguages,
);
}
@override
String toString() => '#$formattedSessionNumber: $word ($partOfSpeech)';
String toString() => '#: ()';
}
extension WordItemLocalization on WordItem {
String get localizedValence => localizeValence(valence);
String get localizedOperator => localizeOperator(operator);
String get localizedFamily => localizeFamily(family);
static String localizeValence(String v) {
switch (v.trim().toLowerCase()) {
case 'active':
return 'Активная';
case 'receptive':
return 'Восприимчивая';
case 'liminal':
return 'Лиминальная';
case 'stable':
return 'Устойчивая';
case 'cutting':
return 'Отсекающая';
case 'binding':
return 'Связующая';
case 'solar':
return 'Солярная';
case 'lunar':
return 'Лунарная';
case 'light':
return 'Светлая';
case 'dark':
return 'Теневая';
case 'neutral':
return 'Нейтральная';
case 'chthonic':
return 'Хтоническая';
default:
final s = v.trim();
if (s.isEmpty) return '';
return s[0].toUpperCase() + s.substring(1);
}
}
static String localizeOperator(String op) {
switch (op.trim().toLowerCase()) {
case 'substance':
return 'Субстанция';
case 'act':
return 'Действие';
case 'relation':
return 'Связь';
case 'shifter':
return 'Сдвиг';
case 'qualifier':
return 'Качество';
case 'filter':
return 'Фильтр';
case 'pronoun':
return 'Указатель';
case 'number':
return 'Число';
case 'nexus':
return 'Узел';
case 'vector':
return 'Вектор';
case 'catalyst':
return 'Катализатор';
default:
final s = op.trim();
if (s.isEmpty) return '';
return s[0].toUpperCase() + s.substring(1);
}
}
static String localizeFamily(String f) {
switch (f.trim().toLowerCase()) {
case 'totem':
return 'Тотем';
case 'sacred_symbol':
return 'Сакральный символ';
case 'sacred_place':
return 'Сакральное место';
case 'element':
return 'Стихия';
case 'cosmos':
return 'Космос';
case 'body':
return 'Тело';
case 'moral':
return 'Этика';
case 'quality':
return 'Качество';
case 'time_calendar':
return 'Календарь';
case 'time_cycle':
return 'Цикл времени';
case 'cult_object':
return 'Культ';
case 'person_archetype':
return 'Архетип';
case 'social':
return 'Социум';
case 'speech':
return 'Речь';
case 'circumstance':
return 'Обстоятельство';
case 'conjunction':
return 'Связка';
case 'preposition':
return 'Отношение';
case 'polarity':
return 'Полярность';
case 'verb_transform':
return 'Трансформация';
case 'particle':
return 'Частица';
case 'determiner':
return 'Определитель';
case 'adverb_index':
return 'Индекс';
case 'misc':
return 'Разное';
default:
final s = f.trim();
if (s.isEmpty) return '';
return s[0].toUpperCase() + s.substring(1);
}
}
static String localizeSystem(String sys) {
switch (sys.trim().toLowerCase()) {
case 'tarot':
return 'Таро';
case 'runes':
return 'Руны';
case 'lenormand':
return 'Ленорман';
case 'iching':
case 'i ching':
return 'И Цзин';
case 'astrology':
return 'Астрология';
case 'alchemy':
return 'Алхимия';
case 'kabbalah':
return 'Каббала';
case 'ogham':
return 'Огам';
case 'cosmology':
return 'Космология';
case 'slavic':
return 'Славянский канон';
case 'bestiary':
return 'Бестиарий';
case 'dream':
return 'Сновидения';
case 'geomancy':
return 'Геомантия';
default:
final s = sys.trim();
if (s.isEmpty) return '';
return s[0].toUpperCase() + s.substring(1);
}
}
static String cleanTagDetail(String det) {
var s = det.trim();
// Remove Latin transliterations like (Cancer / Вода / Луна) -> (Вода · Луна)
s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([^)]+)\)'), (m) => '(${m.group(1)})');
s = s.replaceAll(' / ', ' · ');
// Geomancy 12 Puer (Юноша) -> 12 Юноша
s = s.replaceAllMapped(RegExp(r'(\d+\s+)[A-Za-z\s_]+\(([А-Яа-яЁё\s·-]+)\)'), (m) => '${m.group(1)}${m.group(2)}');
// Alchemy Огонь (Ignis) -> Огонь
if (!RegExp(r'[ᚠ-ᛪ]').hasMatch(s)) {
s = s.replaceAllMapped(RegExp(r'([А-Яа-яЁё\s]+)\s*\([A-Za-z\s_]+\)'), (m) => m.group(1)!);
}
// Runes: (Kenaz / ᚲ) -> (ᚲ)
s = s.replaceAllMapped(RegExp(r'\([A-Za-z]+\s*[/·]\s*([ᚠ-ᛪ])\)'), (m) => '(${m.group(1)})');
// Clean leftover English in parens
s = s.replaceAll(RegExp(r'\([A-Za-z\s_]+\)'), '');
return s.trim();
}
static String localizeSourceRef(String system, String ref) {
final r = ref.trim().toLowerCase();
const map = <String, String>{
// Runes
'fehu': 'Феху (ᚠ)', 'uruz': 'Уруз (ᚢ)', 'thurisaz': 'Турисаз (ᚦ)', 'ansuz': 'Ансуз (ᚨ)',
'raidho': 'Райдо (ᚱ)', 'kenaz': 'Кеназ (ᚲ)', 'gebo': 'Гебо (ᚷ)', 'wunjo': 'Вуньо (ᚹ)',
'hagalaz': 'Хагалаз (ᚺ)', 'nauthiz': 'Наутиз (ᚾ)', 'isa': 'Иса (ᛁ)', 'jera': 'Йера (ᛃ)',
'eihwaz': 'Эйваз (ᛇ)', 'perthro': 'Перт (ᛈ)', 'algiz': 'Альгиз (ᛉ)', 'sowilo': 'Совило (ᛊ)',
'tiwaz': 'Тейваз (ᛏ)', 'berkano': 'Беркана (ᛒ)', 'ehwaz': 'Эваз (ᛖ)', 'mannaz': 'Манназ (ᛗ)',
'laguz': 'Лагуз (ᛚ)', 'ingwaz': 'Ингваз (ᛜ)', 'dagaz': 'Дагаз (ᛞ)', 'othala': 'Отала (ᛟ)',
// Tarot
'wands': 'Жезлы (Огонь)', 'pentacles': 'Пентакли (Земля)', 'cups': 'Кубки (Вода)', 'swords': 'Мечи (Воздух)',
'fool': 'Шут (0)', 'magician': 'Маг (I)', 'priestess': 'Жрица (II)', 'empress': 'Императрица (III)',
'emperor': 'Император (IV)', 'hierophant': 'Иерофант (V)', 'lovers': 'Влюблённые (VI)', 'chariot': 'Колесница (VII)',
'strength': 'Сила (VIII)', 'hermit': 'Отшельник (IX)', 'wheel': 'Колесо Фортуны (X)', 'justice': 'Справедливость (XI)',
'hanged': 'Повешенный (XII)', 'death': 'Смерть (XIII)', 'temperance': 'Умеренность (XIV)', 'devil': 'Дьявол (XV)',
'tower': 'Башня (XVI)', 'star': 'Звезда (XVII)', 'moon': 'Луна', 'sun': 'Солнце',
'judgement': 'Суд (XX)', 'world': 'Мир (XXI)',
// I Ching
'li': 'Ли ☲ (Огонь)', 'kan': 'Кань ☵ (Вода)', 'xun': 'Сюнь ☴ (Ветер)', 'kun': 'Кунь ☷ (Земля)',
'qian': 'Цянь ☰ (Небо)', 'zhen': 'Чжэнь ☳ (Гром)', 'gen': 'Гэнь ☶ (Гора)', 'dui': 'Дуй ☱ (Водоём)',
'kan_repeat': 'Кань ䷜ (Повторная опасность)', 'pi': 'Пи ䷋ (Упадок)', 'tai': 'Тай ䷊ (Расцвет)',
'gu': 'Гу ䷑ (Исправление)', 'fu': 'Фу ䷗ (Возврат)', 'meng': 'Мэн ䷃ (Юность)', 'ge': 'Гэ ䷰ (Смена)',
'wei_ji': 'Вэй-цзи ䷿ (Ещё не конец)',
// Alchemy
'ignis': 'Огонь', 'aqua': 'Вода', 'aer': 'Воздух', 'terra': 'Земля',
'sulfur': 'Сера (Душа)', 'mercury': 'Ртуть (Дух)', 'sal': 'Соль (Тело)',
'albedo': 'Альбедо (Белизна)', 'nigredo': 'Нигредо (Чернота)', 'rubedo': 'Рубедо (Краснота)',
'ash': 'Пепел', 'luna': 'Луна (Серебро)', 'egg': 'Философское Яйцо', 'vessel': 'Сосуд Гермеса', 'forge': 'Горн',
// Astrology
'comet': 'Комета', 'eclipse': 'Затмение', 'dawn': 'Рассвет', 'dusk': 'Закат',
'zenith': 'Зенит', 'nadir': 'Надир', 'constellation': 'Созвездие', 'planet': 'Планета', 'meteor': 'Метеор',
'equinox': 'Равноденствие', 'orbit': 'Орбита', 'libra': 'Весы', 'aries': 'Овен', 'taurus': 'Телец',
'gemini': 'Близнецы', 'cancer': 'Рак', 'leo': 'Лев', 'virgo': 'Дева', 'scorpio': 'Скорпион',
'sagittarius': 'Стрелец', 'capricorn': 'Козерог', 'aquarius': 'Водолей', 'pisces': 'Рыбы',
// Bestiary
'dog': 'Собака (Страж)', 'snake': 'Змея (Мудрость)', 'eagle': 'Орёл (Высота)', 'wolf': 'Волк (Чутьё)',
'raven': 'Ворон (Память)', 'lion': 'Лев (Царственность)', 'owl': 'Сова (Ночное зрение)',
'serpent_feather': 'Пернатый Змей', 'dragon': 'Дракон', 'phoenix': 'Феникс', 'bear': 'Медведь (Сила)',
'fox': 'Лиса (Хитрость)', 'stag': 'Олень (Благородство)', 'horse': 'Конь (Порыв)', 'spider': 'Паук (Нить судьбы)',
// Virtues & Archetypes
'courage': 'Мужество', 'charity': 'Милосердие', 'karma': 'Карма / Воздаяние', 'guilt': 'Вина',
'shame': 'Стыд', 'pride': 'Гордыня', 'humility': 'Смирение', 'envy': 'Зависть', 'vow': 'Обет',
'greed': 'Алчность', 'gluttony': 'Чревоугодие', 'sloth': 'Уныние', 'wrath': 'Гнев', 'lust': 'Страсть',
'donor': 'Даритель', 'dispatcher': 'Отправитель', 'shadow': 'Тень', 'hero': 'Герой', 'self': 'Самость',
'helper': 'Помощник', 'witch': 'Ведунья', 'princess': 'Царевна', 'exile': 'Изгнанник',
'false_hero': 'Ложный герой', 'judge': 'Судия', 'trickster': 'Трикстер',
// Slavic
'mesyats': 'Месяц ясный', 'oak': 'Священный Дуб', 'willow': 'Плакучая Ива', 'birch': 'Берёза',
'ivy_field': 'Чисто поле', 'crossroads': 'Перепутье', 'threshold': 'Порог', 'well': 'Студёный колодец', 'hearth': 'Родной очаг',
// Ogham
'dair': 'Дуб (Даир / ᚇ)', 'sail': 'Ива (Салье / ᚄ)', 'beith': 'Берёза (Бэйт / ᚁ)', 'gort': 'Плющ (Горт / ᚌ)',
// Symbols & Dreams
'fire': 'Огонь', 'water': 'Вода', 'air': 'Воздух', 'earth': 'Земля',
'key': 'Ключ от врат', 'spark': 'Живая искра', 'path': 'Тропа / Путь', 'book': 'Книга судеб',
'blood': 'Кровь / Род', 'heart': 'Сердце',
'fire_d': 'Сон об огне', 'water_d': 'Сон о глубине', 'death_d': 'Сон о переходе', 'key_d': 'Сон о тайне',
'path_d': 'Сон о странствии', 'child_d': 'Сон о дитяти', 'mirror_d': 'Сон о зеркале', 'wolf_d': 'Сон о звере',
'dream_fire': 'Сон об огне',
};
if (map.containsKey(r)) return map[r]!;
final s = ref.trim().replaceAll('_', ' ');
if (s.isEmpty) return '';
return s[0].toUpperCase() + s.substring(1);
}
}