openspec: архив 14 завершённых change-ов (веб-фиксы, crawler-queue, own-content-hub, publisher-service); спеки влиты в openspec/specs

This commit is contained in:
kpa39l
2026-09-16 17:01:00 +00:00
parent 584582a48c
commit 771f6a8276
88 changed files with 1632 additions and 3 deletions
@@ -0,0 +1,30 @@
## Purpose
Классификация постов по направлениям локальным LLM + словарный фильтр. Дополняется
приоритетом «своего контента» и поддержкой мультинаправлений для fan-out.
## ADDED Requirements
### Requirement: Приоритет своего контента
Классификатор MUST обрабатывать посты с `is_own=1` как «сильные кандидаты»: при наличии
направления по словарю — relevance=critical, classified=True — даже если LLM недоступна
(фолбэк без LLM, пост не теряется). Для внешних постов поведение без изменений.
#### Scenario: Свой пост, LLM недоступна
- **WHEN** пост is_own=1, словарь дал направление, но Ollama недоступна
- **THEN** пост получает direction (словарь), relevance=critical, classified=True,
method='dict-own' (не требует LLM)
#### Scenario: Свой пост без направления по словарю
- **WHEN** пост is_own=1, словарь не дал направление
- **THEN** классификации нет (classified=False) до LLM; пост не теряется (остаётся в очереди)
### Requirement: Мультинаправления
Классификатор MUST уметь возвращать несколько направлений для поста (для маппинга fan-out);
основное направление хранится в posts.direction, дополнительные — в classifications
(таблица уже позволяет несколько классификаций на пост).
#### Scenario: Пост про Linux + AI
- **WHEN** пост упоминает и линукс, и нейросети
- **THEN** в classifications может быть несколько записей (linux, ai); fan-out использует
оба при подтверждении
@@ -0,0 +1,21 @@
## Purpose
Банк статей: markdown-бандлы с frontmatter + медиа. Дополняется пометкой происхождения
своего контента и ссылкой на оригинал.
## ADDED Requirements
### Requirement: Атрибуция в бандле
Бандл поста с is_own=1 MUST содержать в frontmatter `origin: own`, ссылку на оригинал
(`source_url` = t.me/dedinit/<id>) и имя автора («Дед в АйТи»). Бандл внешнего поста —
как раньше (origin: external, source_url=url источника).
#### Scenario: Бандл своего поста
- **WHEN** create_bundle вызывается для поста is_own=1
- **THEN** frontmatter содержит origin: own, source: dedinit, source_url:
https://t.me/dedinit/<tg_post_id>, author: Дед в АйТи
#### Scenario: Бандл внешнего поста в нескольких направлениях
- **WHEN** пост (свой или внешний) опубликован в несколько направлений
- **THEN** бандл создаётся по каждому направлению (bundles/<dir>/<YYYY-MM>/<slug>.md),
обе записи ссылаются на один и тот же original post_id
@@ -0,0 +1,32 @@
## Purpose
Чтение публичных Telegram-каналов через Telethon (MTProto) для сбора новостей с метриками
популярности. Дополняется поддержкой «своих» источников (own: true) — контент пользователя.
## ADDED Requirements
### Requirement: Источники своего контента
Краулер MUST распознавать источники с `own: true` в sources.yaml и для их постов
проставлять `is_own=1`, `is_own_canonical=1`. Для таких источников медиа MUST
скачиваться (контент принадлежит пользователю).
#### Scenario: Краулинг своего канала
- **WHEN** источник slug=dedinit имеет own: true
- **THEN** посты сохраняются с is_own=1 и is_own_canonical=1; медиа скачивается;
инкрементальный обход и дедуп работают как обычно
#### Scenario: Чужой форвард в своём канале
- **WHEN** пост в своём канале — форвард из чужого канала
- **THEN** пост сохраняется с is_own=1 (это пост пользователя, он его переслал) и с
fwd_from_channel_id/fwd_from_post_id; медиа не скачивается (содержимое чужое),
атрибуция оригинала сохраняется в fwd-полях
### Requirement: Канонический экземпляр своего контента
Если пост пользователя (is_own=1) позже встречается во внешнем канале (тот же sha256/url),
внешний экземпляр MUST сохраняться как «упоминание» с is_own=0; каноническим остаётся
первичный (is_own_canonical=1).
#### Scenario: Свой пост запостили в чужой канал
- **WHEN** краулер находит в чужом канале пост с текстом, совпадающим с is_own-постом
- **THEN** создаётся запись is_own=0 (упоминание) без дублирования контента; веб видит
оба экземпляра, но кандидатом на публикацию считается канонический (is_own_canonical)
@@ -0,0 +1,43 @@
## Purpose
Публикация отобранных новостей через Bot API в тематические Telegram-каналы. Дополняется
автораспространением своего контента (fan-out) по нескольким направлениям.
## ADDED Requirements
### Requirement: Автораспространение (fan-out)
Подтверждение СВОЕГО поста (is_own=1) MUST публиковать карточку во ВСЕ тематические
каналы @dedinit_vesti_<direction>_<lang>_bot, соответствующие выбранным направлениям
(по умолчанию — все направления классификации поста). Каждая карточка MUST содержать
атрибуцию «Дед в АйТи» (@dedinit) и ссылку на оригинал t.me/dedinit/<post_id>.
#### Scenario: Мульти-публикация
- **WHEN** подтверждается свой пост с направлениями [linux, ai]
- **THEN** карточка отправляется в @dedinit_vesti_linux_ru_bot и @dedinit_vesti_ai_ru_bot;
обе содержат ссылку на оригинал
#### Scenario: Чужой пост — без fan-out
- **WHEN** подтверждается внешний пост (is_own=0)
- **THEN** публикуется только в канал своего направления, без атрибуции автора
### Requirement: Публикация по списку направлений
Публикатор MUST поддерживать `publish_multi(directions)` — публикацию карточки по списку
направлений, возвращающую map {direction: message_id}. При пустом списке направлений
MUST публиковать в направление по умолчанию (direction поста).
#### Scenario: Список направлений рассылки
- **WHEN** publish_multi вызывается с directions=[linux, ai]
- **THEN** возвращается {linux: message_id1, ai: message_id2}; каждая публикация
записывается в published с distributed_dirs
#### Scenario: Пустой список направлений
- **WHEN** publish_multi вызывается без directions
- **THEN** публикация идёт в канал направления поста (direction по умолчанию), без fan-out
### Requirement: Метрики по каждому направлению
Для опубликованных карточек MUST собираться views через Bot API по каждому направлению
(каждому message_id), чтобы веб показывал эффективность рассылки по лентам.
#### Scenario: Метрики fan-out
- **WHEN** пост разослан в [linux, ai] и каналы набирают просмотры
- **THEN** get_views вызывается для каждого message_id; views хранятся по направлению
@@ -0,0 +1,28 @@
## Purpose
Веб-интерфейс управления VESTI. Дополняется фильтром «Свои», бейджем и выбором
направлений рассылки для своего контента.
## ADDED Requirements
### Requirement: Фильтр «Свои»
Веб MUST давать фильтр постов по `is_own` (все/только свои/только внешние) и показывать
бейдж «СВОЙ» у постов is_own=1. Для своего поста при подтверждении MUST отображаться
выбор направлений рассылки (по умолчанию — все направления классификации поста).
#### Scenario: Фильтр своих постов
- **WHEN** админ выбирает фильтр «Свои»
- **THEN** показываются только посты is_own=1 с бейджем «СВОЙ» и чекбоксами направлений
#### Scenario: Подтверждение своего поста
- **WHEN** админ подтверждает свой пост с выбранными направлениями [linux, ai]
- **THEN** публикация идёт в оба канала (fan-out), результат виден в опубликованных с
distributed_dirs
### Requirement: Список распространения
Веб MUST показывать для опубликованного поста, в какие направления/каналы он был
разослан (distributed_dirs) и метрики (views) по каждому каналу.
#### Scenario: Просмотр распространения
- **WHEN** админ открывает опубликованный пост (свой)
- **THEN** видит список @dedinit_vesti_<dir>_<lang>_bot с views по каждому