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
+369
View File
@@ -0,0 +1,369 @@
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<DiveRole> 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<DiveSlot> kDiveLanguages = [
DiveSlot('шумерский', 'шумерский'),
DiveSlot('древнеегипетский', 'древнеегипетский'),
DiveSlot('иврит', 'иврит'),
DiveSlot('древнегреческий', 'древнегреческий'),
DiveSlot('латынь', 'латынь'),
DiveSlot('древнеирландский', 'древнеирландский'),
DiveSlot('древнескандинавский', 'древнескандинавский'),
DiveSlot('старославянский', 'старославянский'),
DiveSlot('санскрит', 'санскрит'),
DiveSlot('авестийский', 'авестийский'),
DiveSlot('древнекитайский', 'древнекитайский'),
DiveSlot('японский', 'японский'),
DiveSlot('арабский', 'арабский'),
DiveSlot('майя', 'майя'),
DiveSlot('церковнославянский', 'церковнославянский'),
];
const List<DiveSlot> 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<String, String> _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<String, String> reports,
required String core,
required String oracle,
required String shadow,
required String asks,
required String step,
required String binds,
required Map<String, String> cultures,
required Map<String, String> languages,
required Map<String, String> traditions,
}) {
final payload = <String, dynamic>{
'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<String> matchCultureNames(String answer) {
final found = <String>[];
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<String> matchLanguageNames(String answer) {
final known = {for (final slot in kDiveLanguages) slot.id: slot.id};
final found = <String>[];
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<String>? binds;
final Map<String, String>? cultures;
final Map<String, String>? languages;
final List<SymbolLayer>? 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<String>? 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<String, String>? 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<String, String>? 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<SymbolLayer>? 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,
);
}