import 'dart:convert'; import '../models/culture_branch.dart'; import '../models/word_model.dart'; /// Роли ручного бурения. Человек сам отвечает вместо моделей. class DiveRole { final String id; final String title; final String hint; const DiveRole(this.id, this.title, this.hint); } const List kDiveRoles = [ DiveRole( 'route_cultures', 'Маршрут культур', 'Три традиции, где у знака есть именной след. Ответ — названия через запятую.', ), DiveRole( 'route_languages', 'Маршрут языков', 'Три языка с прямым корнем или устойчивым именем образа. Ответ — названия через запятую.', ), DiveRole( 'mystic', 'Мистик', 'Закон знака: устройство, свет, тень и какие системы держат именно это слово.', ), DiveRole( 'psycho', 'Психоаналитик', 'Один психический механизм этого знака, не архетип вообще.', ), DiveRole( 'synth', 'Синтезатор', 'Сжимает уже вписанные ответы в карточку. JSON можно разложить по полям.', ), ]; class DiveSlot { final String id; final String title; const DiveSlot(this.id, this.title); } const List kDiveLanguages = [ DiveSlot('шумерский', 'шумерский'), DiveSlot('древнеегипетский', 'древнеегипетский'), DiveSlot('иврит', 'иврит'), DiveSlot('древнегреческий', 'древнегреческий'), DiveSlot('латынь', 'латынь'), DiveSlot('древнеирландский', 'древнеирландский'), DiveSlot('древнескандинавский', 'древнескандинавский'), DiveSlot('старославянский', 'старославянский'), DiveSlot('санскрит', 'санскрит'), DiveSlot('авестийский', 'авестийский'), DiveSlot('древнекитайский', 'древнекитайский'), DiveSlot('японский', 'японский'), DiveSlot('арабский', 'арабский'), DiveSlot('майя', 'майя'), DiveSlot('церковнославянский', 'церковнославянский'), ]; const List kDiveTraditions = [ DiveSlot('runes', 'Руны'), DiveSlot('tarot', 'Таро'), DiveSlot('ogham', 'Огам'), DiveSlot('kabbalah', 'Каббала'), DiveSlot('lenormand', 'Ленорман'), DiveSlot('geomancy', 'Геомантия'), DiveSlot('iching', 'И Цзин'), DiveSlot('alchemy', 'Алхимия'), DiveSlot('astrology', 'Астрология'), DiveSlot('cosmology', 'Космология'), DiveSlot('slavic', 'Славянский канон'), ]; const Map _languageAliases = { 'греческий': 'древнегреческий', 'китайский': 'древнекитайский', 'египетский': 'древнеегипетский', 'скандинавский': 'древнескандинавский', 'ирландский': 'древнеирландский', 'славянский': 'старославянский', 'церковный славянский': 'церковнославянский', }; String _dossier(WordItem word) { final pos = word.partOfSpeech.trim().isEmpty ? 'не указана' : word.partOfSpeech.trim(); final category = word.category.trim().isEmpty ? 'не указана' : word.category.trim(); final buffer = StringBuffer() ..writeln('Знак Оракула 999: «${word.word}».') ..writeln('Часть речи: $pos. Категория: $category.'); if (word.core.trim().isNotEmpty) { buffer.writeln('Текущее ядро, его можно уточнить, но не подменяй другим знаком: ${word.core.trim()}'); } return buffer.toString().trimRight(); } String cultureRoutePrompt(WordItem word) { final menu = kCultureBranches.map((b) => b.name).join('; '); return '${_dossier(word)}\n' 'Выбери ровно 3 традиции, где у ЭТОГО образа есть именной миф, текст или обряд, ' 'а не просто «похожая идея».\n' 'Только из списка: $menu.\n' 'Ответ: три названия через запятую. Без пояснений.'; } String languageRoutePrompt(WordItem word) { final menu = kDiveLanguages.map((b) => b.title).join('; '); return '${_dossier(word)}\n' 'Выбери ровно 3 языка, где есть прямой корень, устойчивый перевод этого образа ' 'или его иероглиф. Не выбирай язык только потому, что он древний.\n' 'Только из списка: $menu.\n' 'Ответ: три названия через запятую. Без пояснений.'; } String mysticPrompt(WordItem word) { return 'Ты мистик Оракула 999. Разбираешь один знак как закон, не как гороскоп. ' 'Пиши по-русски. Не предсказывай любовь, деньги и здоровье. ' 'Не занимай чужую карту, руну или гексаграмму, если этот знак ею не является. ' 'Если след тонок — так и скажи.\n\n' '${_dossier(word)}\n' 'Дай: устройство образа; светлую сторону того же закона; теневую сторону того же закона; ' 'какие системы (Таро, руны, Ленорман, геомантия, И Цзин, алхимия, астрология) ' 'держат именно это слово, а какие только соседние. Соседние не присваивай.'; } String psychoPrompt(WordItem word) { return 'Ты юнгианский аналитик. Один знак — один психический механизм. ' 'Пиши по-русски, без лекции про индивидуацию вообще. ' 'Назови, что человек делает с этим образом, когда он интегрирован, ' 'и что делает, когда вытесняет.\n\n' '${_dossier(word)}\n' 'Разбери только его, не архетип «вообще».'; } String cultureSlicePrompt(WordItem word, String culture) { return 'Ты мифолог одной традиции: $culture. Другие культуры запрещены. ' 'Пиши по-русски. Если у традиции нет достоверного следа этого образа, ' 'ответь ровно фразой «следа нет» и остановись. Не заполняй пустоту соседним мифом. ' 'Нужны: исконное слово или знак, транслитерация, имя текста или мифа, ' 'что образ делает, чего не позволяет.\n\n' '${_dossier(word)}\n' 'Традиция: $culture.'; } String languageSlicePrompt(WordItem word, String language) { return 'Ты этимолог одного языка: $language. Другие языки запрещены. ' 'Пиши по-русски. Если у русского слова нет корня в этом языке, ' 'скажи «прямого корня нет» и приведи только ближайшее культурное слово этого языка ' 'для того же предмета, не выдавая его за этимон. ' 'Нужны: форма, транслитерация, сдвиг значения, один источник или памятник.\n\n' '${_dossier(word)}\n' 'Язык: $language.'; } String traditionSlicePrompt(WordItem word, String tradition) { return 'Ты хранитель одной системы: $tradition. Другие системы запрещены. ' 'Пиши по-русски. Если этот знак сам не является картой, руной, фигурой или станцией ' 'этой системы, не занимай чужую станцию: напиши, чем знак лишь сосед, ' 'или что прямого места нет.\n\n' '${_dossier(word)}\n' 'Система: $tradition.\n' 'Ответ: 2–5 предложений закона именно этого слова внутри системы.'; } String rolePrompt(String roleId, WordItem word) { switch (roleId) { case 'route_cultures': return cultureRoutePrompt(word); case 'route_languages': return languageRoutePrompt(word); case 'mystic': return mysticPrompt(word); case 'psycho': return psychoPrompt(word); default: return _dossier(word); } } String synthesizerPrompt({ required WordItem word, required Map reports, required String core, required String oracle, required String shadow, required String asks, required String step, required String binds, required Map cultures, required Map languages, required Map traditions, }) { final payload = { 'word': word.word, 'reports': reports, 'draft': { 'core': core, 'oracle': oracle, 'shadow': shadow, 'asks': asks, 'step': step, 'binds': binds, 'cultures': cultures, 'languages': languages, 'traditions': traditions, }, }; return 'Ты архитектор Оракула 999. Сожми уже собранные ответы в один JSON без markdown. ' 'Не выдумывай культуру, язык или систему, если во входе пусто или стоит «следа нет».\n' 'Ключи: word, core, oracle, shadow, asks, step, binds, layers, cultures, languages.\n' 'core — одно предложение, закон знака.\n' 'oracle — устройство знака, сосед в поле и цена. Культуры поимённо сюда не копируй.\n' 'shadow — обратная сторона того же закона.\n' 'asks — один вопрос. step — один физический жест, не «подумай».\n' 'binds — до 6 русских слов знаков из тысячи, не сам знак.\n' 'layers — список {system, title, law}. system из: ' '${kDiveTraditions.map((s) => s.id).join(', ')}.\n' 'cultures — объект. Ключ строго из списка традиций. Значение: 2–5 фраз.\n' 'languages — объект. Ключ — имя языка, значение — этимологическая заметка.\n' 'Запрещены заготовки: «сакральный символ», «созерцать символ», ' '«готов ли ты принять этот урок», «ключевую сферу кристаллизации».\n' 'Никакого текста вокруг JSON.\n\n' '${_dossier(word)}\n' '${jsonEncode(payload)}'; } /// Имена из ответа маршрута → точные ключи реестра. List matchCultureNames(String answer) { final found = []; for (final raw in answer.split(RegExp(r'[,;\n]+'))) { final token = raw.trim(); if (token.isEmpty) continue; final branch = cultureBranchByName(token); if (branch != null && !found.contains(branch.name)) found.add(branch.name); } return found; } List matchLanguageNames(String answer) { final known = {for (final slot in kDiveLanguages) slot.id: slot.id}; final found = []; for (final raw in answer.split(RegExp(r'[,;\n]+'))) { final token = raw.trim().toLowerCase(); if (token.isEmpty) continue; final name = known[token] ?? _languageAliases[token]; if (name != null && !found.contains(name)) found.add(name); } return found; } class DiveCanonDraft { final String? core; final String? oracle; final String? shadow; final String? asks; final String? step; final List? binds; final Map? cultures; final Map? languages; final List? layers; const DiveCanonDraft({ this.core, this.oracle, this.shadow, this.asks, this.step, this.binds, this.cultures, this.languages, this.layers, }); } DiveCanonDraft? parseDiveCanon(String text) { var src = text.trim(); if (src.isEmpty) return null; if (src.startsWith('```')) { src = src.replaceFirst(RegExp(r'^```[a-zA-Z]*\n?'), ''); if (src.endsWith('```')) src = src.substring(0, src.length - 3).trim(); } final start = src.indexOf('{'); final end = src.lastIndexOf('}'); if (start < 0 || end <= start) return null; final dynamic decoded; try { decoded = jsonDecode(src.substring(start, end + 1)); } catch (_) { return null; } if (decoded is! Map) return null; final map = decoded.map((key, value) => MapEntry(key.toString(), value)); List? binds; final rawBinds = map['binds']; if (rawBinds is List) { binds = rawBinds.map((e) => e.toString().trim()).where((e) => e.isNotEmpty).toList(); } else if (rawBinds is String && rawBinds.trim().isNotEmpty) { binds = rawBinds.split(RegExp(r'[,;]+')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); } Map? cultures; if (map['cultures'] is Map) { cultures = {}; for (final entry in (map['cultures'] as Map).entries) { final branch = cultureBranchByName(entry.key.toString()); final key = branch?.name ?? entry.key.toString().trim(); final value = entry.value?.toString().trim() ?? ''; if (key.isNotEmpty && value.isNotEmpty && value != 'следа нет') cultures[key] = value; } } Map? languages; if (map['languages'] is Map) { languages = {}; for (final entry in (map['languages'] as Map).entries) { final names = matchLanguageNames(entry.key.toString()); final key = names.isEmpty ? entry.key.toString().trim().toLowerCase() : names.first; final value = entry.value?.toString().trim() ?? ''; if (key.isNotEmpty && value.isNotEmpty) languages[key] = value; } } List? layers; if (map['layers'] is List) { layers = []; for (final item in map['layers'] as List) { if (item is! Map) continue; final law = item['law']?.toString().trim() ?? ''; if (law.isEmpty) continue; final systemRaw = (item['system'] ?? item['title'] ?? '').toString().trim().toLowerCase(); DiveSlot? slot; for (final candidate in kDiveTraditions) { if (candidate.id == systemRaw || candidate.title.toLowerCase() == systemRaw) { slot = candidate; break; } } if (slot == null) continue; layers.add(SymbolLayer(system: slot.id, title: slot.title, law: law)); } } String? textOf(String key) { final value = map[key]; if (value == null) return null; final text = value.toString().trim(); return text.isEmpty ? null : text; } return DiveCanonDraft( core: textOf('core'), oracle: textOf('oracle'), shadow: textOf('shadow'), asks: textOf('asks'), step: textOf('step'), binds: binds, cultures: cultures, languages: languages, layers: layers, ); }