extract_chunk(chunk_xml, system_prompt, known_items) → ChunkSummary — map step через tool-use emit_chunk_summarysynthesize_digest(chunk_summaries, top_messages, system_prompt) → Digest — reduce step через tool-use emit_digestopenai.AsyncOpenAI(base_url=DIGEST_LLM_BASE_URL, api_key=DIGEST_LLM_API_KEY)tools параметр (OpenAI формат, CLIProxy транслирует)extra_headers: {"anthropic-beta": "prompt-caching-2024-07-31"}DIGEST_MAP_MODEL / DIGEST_REDUCE_MODELemit_chunk_summary и emit_digestchunk_summary (string, max 1200), signal_quality (enum + reason)topics (max 6), decisions (max 5), action_items (max 8), open_questions (max 6)links (max 15), artifacts (max 10), knowledge_items (max 15)notable_message_ids (max 10)content_pointer: {message_id, char_start, char_end} — для verbatim (промпты, конфиги, код)content: string — для synthesized (рекомендации, troubleshoot)content_pointer и content взаимоисключающиеcontext_version — какая модель/версия, entity_refs — канонические сущностиsupersedes_id, conflicts_with_id — связи между itemsmessage_idstitle (max 120), overview (max 2400, markdown)highlights (max 5), coverage, search_keywords (max 10)models.py — source of truth{message_id, char_start, char_end} → substring из исходного сообщенияmessage_id существует в окнеchar_start < char_end, оба в пределах len(normalized_text)normalized_text (что видит LLM), не на raw_contentmessage_id должен существовать в input. + ≥50% n-gram overlap между cited message и claimsignal_quality == "empty" → все массивы пустыеcontext_version для version-sensitive kinds → flagextraction_failed, partial output с flagsrun_digest() — полный flow от загрузки до сохраненияdiscord_digestchannel.last_digested_idDigestJob создаётся в начале, статус обновляется по ходуFeedGatheringDigest — полный дайджест со всеми полямиFeedGatheringDigestSummary — лёгкий для списка (title, excerpt, signal quality, counts)FeedGatheringKnowledgeItem — с content или resolved pointer contentFeedGatheringDecision, ActionItem, OpenQuestionFeedGatheringDigestJob — статус и статистикаFeedGatheringTopicProfile, FeedGatheringEntitytriggerFeedGatheringDigest(channelId) → создаёт job, будит runner, возвращает job для pollingfeedGatheringDiscordDigests(filter) → keyset pagination, фильтры: server/channel/date/signal_qualityfeedGatheringDiscordDigest(id) → single digest с полной детализациейfeedGatheringDigestJobs(channelId) → недавние jobs для каналаdocs/gold-set/test_digest_pipeline.py — полный flow с mock openai.AsyncOpenAI: preprocess → chunk → mock extract → validate → mock reduce → storetest_digest_validation.py — fabricated message IDs, pointer out of bounds, empty citations, signal consistency, language mismatchtest_digest_pointers.py — valid bounds, edge cases (0-start, end-of-message), unicode, code blockstest_digest_schemas.py — Pydantic models и JSON tool schemas совпадаютВызвать triggerFeedGatheringDigest(channelId) через GraphiQL → получить полный дайджест реального канала.
Pointer extraction работает побайтово. Валидация ловит fabricated IDs и невалидные pointers.
Baseline P/R/F1 измерен на gold set.