Files
board-viewer/tests/fixtures/real-books-STATUS.md
vitya d093693795 feat(reader): Gitea client + config + board orchestrator
closes board-viewer-gitea-reader

- src/gitea.ts: HTTP client (getFile, getLatestCommitIso, rawUrl); DI'd fetch
- src/reader.ts: readBoard(client, repos) → TaskRecord[]
- src/config.ts: TOML config loader with board_viewer_repos whitelist
- 25 tests total (9 parser incl. 3 real-repo fixtures, 7 gitea, 5 reader, 4 config)
- emoji contract 🔴🟡🔵🟢 (open/in_progress/paused/blocked/done)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:48:37 +03:00

557 lines
98 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task Board
_Updated: 2026-05-21 — `drain-bookva-ym-stocks` 🟢 **DONE end-to-end**. Bookva-Ozon FBS: 5173/5184 → stock=0 (5171 первый прогон + 2 ручной multi-warehouse патч К000405/К000632 на Афонская КГТ + realFBS). YM Bookva: 1049/1049 → count=0. Slovo (id=2) не затронут, торгует штатно: `ozon stocks syncronization` каждую минуту SUCCESS (ctx.data{idSeller:2, otherSellers:[]} читается корректно), `sync stocks with warehouse` идёт. 14 commits на master (включая 4 hot-fix циклов SDK): план/спека → guard fbsPostings → handler TDD (6 unit-тестов) → tasks.json drop YM-cron → drain CLI (7 unit-тестов, basePath + IN_SALE/PUBLISHED фильтры + @books/yandex-market-partner-api wrapper + bypass YM `updatedAt` SDK-bug + multi-warehouse enumeration). YM Agenda docs cleanup: 8 (2 активных + 6 stale dup от старого idempotency) → 0 через mongosh deleteMany. **Leftover: 11 SKU на FBO в Bookva-Ozon** (147720748, 6281157, Б400095, 19687905, Б2003910, Б401223, 2888034, 7754504, К000370, Б601643, Б604747) — обнулить через API продавцу невозможно (склады Ozon), нужен FBO withdrawal в кабинете руками. Bug в `@books/yandex-market-partner-api/stocks.js:59` (updatedAt double-toISOString, идентичен 2026-04-27 fix'у в api service но в shared wrapper не починен) занесён в [business-logic-bugs.md](business-logic-bugs.md). Spec: [.wiki/concepts/drain-bookva-ym-stocks.md](../.wiki/concepts/drain-bookva-ym-stocks.md), plan: [.tasks/drain-bookva-ym-stocks-plan.md](drain-bookva-ym-stocks-plan.md)._
_Updated: 2026-05-21 — заведена `drain-bookva-ym-stocks` 🔴 active. Soft drain каналов Bookva-Ozon (`id_seller=1`) и Yandex.Market (тот же seller=1, `ym_seller=1`): push stocks=0/count=0 через API + reconfig scheduler так, чтобы автосинхронизация не возвращала ненулевые значения. Slovo (`id_seller=2`) торгует штатно. 6 изменений: (1) guard в `packages/utils/fbsPostings.js` `length > 0` — закрывает YM-leak через `updateSellersProductsStocks→calculateAndUpdateYmStocks(1)` который дёргается даже при `otherSellers=[]`; (2) `task-runner/tasks/ozonStocksSyncronization.js` читает `ctx.data().idSeller` + `otherSellers`; (3) `tasks.json` data `{idSeller:2, otherSellers:[]}` для obo task'ов; (4) drop YM-cron'ов (`ym stocks syncronization` + `sync ym stocks with warehouse`) из Agenda; (5) one-off CLI `packages/tools/drain-bookva-ym-stocks.js` (hybrid CLI+module, dry-run, --only ozon|ym, `.bookva.cmd`). Spec: [.wiki/concepts/drain-bookva-ym-stocks.md](../.wiki/concepts/drain-bookva-ym-stocks.md). Без архива/`has_stock=false`/выключения канала — только stocks, обратимо. Кода ещё нет — план следующим шагом через `superpowers:writing-plans`._
_Updated: 2026-05-21 — `move-promo-bookva-to-slovo` 🟢 **DONE end-to-end**. Bookva долгопрод (promo id=1, 663 active applied, -30%) свёрнут, перенесён на Slovo (новая id=3, 660 SKU, -30%, до 2026-06-04 14:00 MSK). По ходу всплыл **3-й bug в domain-promotion CLI**`ozon-promotion-deactivate.js:137` сравнивает `expectedPrice = snapshotPrice` (вместо `actionPrice` по спеке [[domain-promotions]]) → 663 SKU помечены diverged, `importPrices` не вызван, Bookva-Ozon цены остались -30%, БД ушла в фантомное ended. Switch на ad-hoc oneshot `packages/tools/_push-prices-from-snapshot.mjs` (CLI: --source-promotion-id --target-channel-id [--mark-restored] [--chunk-size]) — обход defensive, прямой `applyPriceToOzon` + sync БД. Прогон: Step1 Bookva real-restore 663/663, Step2 Slovo cohort-restore 235/235, Step3 Bookva-snap→Slovo 660/663 (3 NOT_FOUND: `19119894`, `Б2008714`, `Б607029` — нет в Slovo каталоге), Step4 create id=3, Step5 add 660/660, Step6 activate 660/660 applied. Bug в deactivate занесён в [business-logic-bugs.md](business-logic-bugs.md) (запись 2026-05-21). Файл `_push-prices-from-snapshot.mjs` оставлен в `packages/tools/` (без хардкод-кред, через `salesChannel.getSeller()`) — пригодится для будущих cross-shop миграций или fix-сессий deactivate.js._
_Updated: 2026-05-20 — заведена `fbs-picking-list-pdf-thumbs` 🔴 active. Bugfix на `fbs-picking-list-pdf`: prod run #37944 (idPickingList=162) — email с 38MB PDF режется SMTP-сервером (552 5.3.4). Корень — `FbsPickingPlacePrint.vue:14` ставит `<img :src="item.image" />` на CDN full-res JPG, puppeteer встраивает в PDF без даунсемпла (100 items × ~300KB = 30-40MB). Fix — новый thumb-endpoint `/images/thumb` в `@books/api` (sharp resize до 240px JPEG q80), allowlist `*.ozone.ru` (SQL подтверждает 105 347 рядов в `cdn1.ozone.ru` + `nowatermark.ozone.ru`), auth тот же `?token=`. PDF упадёт до ~1-3MB, email пройдёт. TDD: pure-logic `parseThumbParams`+`isAllowedHost` (security/SSRF, rules 3+4). Bump `@books/api` 2.1.0→2.2.0, `@books/web` 2.1.0→2.2.0._
_Updated: 2026-05-17 — `slovo-product-creation-recovery` 🟢 **DONE end-to-end**. После закрытия `slovo-in-sale-gap-diagnose` Slovo 4742 → **5153** (+411, модерация Pass 1 #2 досревала), gap 676 → **306** (95.3% от Bookva 5406). Остаточные 245 «out_of_stock» — артефакт priority chain bucketize'а в diag tool (`has_stock=false` сел приоритетом выше `is_created=false`); реально Ozon отвергает `updateProductsStocks2` с `PRODUCT_IS_NOT_CREATED` пока карточка не сolzит `is_created=true`. Concept [ozon-product-creation-blockers](../.wiki/concepts/ozon-product-creation-blockers.md) (ингестнут вчера, commit ec3d596) расписан → реализованы 2 tool'а: **Pass 3** `products-fix-creation-errors-from-mirror` (парсит `errors[].attribute_id` по whitelist кодов default `CONDITIONAL_ATTRIBUTE_ERROR`, копирует значения этих атрибутов из Bookva через `UpdateAttributes`) + **Pass 4** `products-reimport-stuck` (auto-detect stuck `is_created=false` + пустой `errors[]`, re-import через `ImportProductsBySKU` без diff'а). TDD: 10 + 8 unit-тестов. Prod smoke: Pass 3 — 74/74 imported (task_id=4479563894, из 169 targeted Slovo / 102 Bookva matched), Pass 4 — 6/6 imported (task_id=4479600569, из 632 stuck / 626 без Bookva master). Bump `@books/tools` 0.9.0→0.10.0 MINOR. 185/185 tests green. **Lesson:** я слил час на параллельное расследование `slovo-stock-sync-bug` (создал и удалил task) **не прочитав wiki** — concept уже описывал точный fix. Юзер дважды redirect'нул (`не там копаешь``вики читать с самого начала сессии!`). Стоит запомнить как hard rule._
_Updated: 2026-05-16 — `slovo-in-sale-gap-diagnose` 🟢 **DONE**. Diagnostic CLI `analyst/ozon-in-sale-gap.js` (13/13 тестов) выявил два root cause'а: (1) **`sellers.put_on_sale_delay_days=7` для Slovo** — `calculateNewStocks` в `packages/utils/stocks.js` фильтрует `units.put_on_sale_date <= now-7d`, поэтому свежие книги (последний экземпляр, положен <7d назад) не считались available для Slovo → stock=0 → has_stock=false. Юзер поменял delay на 0. Manual trigger `sync stocks with warehouse` (idSeller=2) подтянул **+194 IN_SALE** (4548→4742, 84.0%→87.5%). (2) **Pass 1 фильтр отсекал Б2/К**, юзер: «только У не переносим». Filter обновлён в обоих CLI (`products-import-by-sku.js` + `products-fill-improve-attributes.js`), bump `@books/tools` 0.7.0→0.8.0. Pass 1 #2 с новым фильтром: 444 кандидата отправлено через 5 task_id (443 pending, 1 failed offer_id=3907989, **0 unmatched**) — через 1-3 дня после модерации станут IN_SALE → прогноз ~5180 = ~95.6% от Bookva. Остаточный gap после dust settles: ~232 out_of_stock (5/15 sample — реальные нули, 10/15 — `available>0` но не пушится — потенциальная business-logic-bug в `stocks.js:195` где `productInfo.stocks.present` сравнивается на верхнем уровне, а реально поле внутри `stocks[]`) + ~50 state_failed:imported/validated/declined/etc. Memory: `reference_legacy_offer_id_prefix_filter`, `reference_seller_put_on_sale_delay_days`._
_Updated: 2026-05-15 (Pass 2 closed + verified) — **`ozon-products-copy-bukva-to-slovo` 🟢 DONE.** Full prod run `--limit 4000` → 1179 карточек в UpdateAttributes через 12 task_id, 0 payload-ошибок. Post-модерация замер: avg rating **39.37 → 41.60 (+2.23)**, bucket 8-12 сократился на 372 (карточки перешли в 1-7 improve), bucket 13+ на 23. Эффект verified программно через повторный `GetProductRatingBySku`. Sync `product_sales_channels` Slovo (id=2) — 571 новых связей создано, 183 ghost-карточки пропущены. 5 commits локально pending user-go на push._
_Updated: 2026-05-15 (вечер #3) — **Phase 1 (Pass 1) полностью выгружен на проде** под IN_SALE-only решением. Prod batch: 608 IN_SALE-карточек Bookva → Slovo, 7 task_ids, **дубликатов нет** (verified `GetProductList` Slovo + `offer_id` count = 1). Случайно прогнал команду дважды (был баг в моём mental model — `grep` после первого `node` я перечитал как «replay summary»); Ozon во втором прогоне ответил `status:skipped` на все 608 (умно). В Slovo также живут 50 карточек из первого ALL+blacklist smoke (могут быть не-IN_SALE) — чистка отдельной задачей если юзер скажет. Push pending user-go. Phase 2 — отдельной сессией._
_Updated: 2026-05-15 (вечер #2) — **PIVOT** ozon-products-copy-bukva-to-slovo на IN_SALE-only. Юзер: «нужны только те, котрые в Букве в продаже» (и Pass 2 тоже). visibility ALL+blacklist → IN_SALE-only, 2-слойный клиентский фильтр снят (IN_SALE Ozon-side уже отсекает archived/declined/state_failed). Local dry-run после pivot: Bookva IN_SALE **5506** → legacy 4976 → diff vs Slovo 4368 → **608 новых кандидатов** (а не 13832). 4 commits: `0047eea` spec, `b2965f6` code, `c460a6b` README, `c261dd8` bump 0.5.0→0.5.1. **608 — реальный объём Pass 1 на проде; 50 импортнутых из предыдущего batch'а — могли быть не-IN_SALE (вне нового scope), они в Slovo, чистка — отдельно если юзер скажет.** Next: prod batch (юзер сказал «давай, нужно»)._
_Updated: 2026-05-15 (вечер) — `ozon-products-copy-bukva-to-slovo` **Phase 1 done end-to-end**, 6 commits на master, **не push'ено**. Smoke discovery → secondary-фильтр верифицирован (`moderate_status='declined' || status_failed!==''`). Refactor + .cmd + README + bump 0.4.0→0.5.0. Local dry-run: 13832 кандидата → secondary отсёк 131 → cap=50. **Prod smoke `--limit 50` verified программно:** `task_id=4462621390`, **50/50 в `imported`**, `GetProductList` Slovo по тем же offer_id вернул **50/50** ✅. Criterion 10 спеки — done без UI-проверки юзера. Phase 2 (Pass 2 — `products-fill-improve-attributes.js`) — отдельной сессией. Pending: user-go на push._
_Updated: 2026-05-15 (поздний день) — заведена `ozon-products-copy-bukva-to-slovo` 🔴 active. Два прохода CLI в `packages/tools/`: Pass 1 — расширение существующего [products-import-by-sku.js](../packages/tools/products-import-by-sku.js) (visibility=ALL + blacklist archived/not_moderated/state_failed, CLI-флаги для кред вместо хардкода, `.cmd`-обёртка `bookva-to-slovo`); Pass 2 — новый CLI `products-fill-improve-attributes.js` (для существующих в Slovo карточек с непустым `improve_attributes[]` — забирает значения этих атрибутов из Bookva через `GetProductAttributesV4` и пушит через `UpdateAttributes`). Доменная область books не используется — только Ozon Seller API; ключ соответствия — `offer_id`. Префикс-фильтры У/Б2/К — legacy, оставлены, разберёмся потом. Spec: [.wiki/concepts/ozon-products-copy-bukva-to-slovo.md](../.wiki/concepts/ozon-products-copy-bukva-to-slovo.md). Кода ещё нет — юзер ушёл на другую работу, разработка автономная._
_Updated: 2026-05-15 — `analyst-workflow-claude-md` 🟢 **DONE** ✅ (commit `44d97b0`): добавлена секция «Analyst workflow» в корневой [CLAUDE.md](../CLAUDE.md) (39 строк) — триггер-фразы, 3 шага (load design context → check existing → write new), inline-блок стиля (ESM, commander+isDirectRun, JSDoc, bin+exports, ozon-seller-api-client напрямую, sequelize+raw SQL), read-каналы, write-через-CLI, anti-patterns. `tools-ci-path-filter` 🟢 **DONE (частично)** ✅ (commit `102a92e`): сужено до CLI-only-tools подпутей (`analyst/`, `plans/`, `not-for-qwen/`, `output/`, README/QWEN.md, *.cmd) — исключение `packages/tools/` целиком сломало бы prod runtime (fbs-ship/sync-prices импортятся в task-runner+api). Оба коммита локально на master, не push'ены. `analyst-workflow-review` 🔵 remains blocked — будет разблокирован после push'а._
_Updated: 2026-05-14 (ночь) — `ozon-revenue-per-product` 🟢 **DONE end-to-end** ✅. CLI-tool [`packages/tools/ozon-revenue-per-product.js`](../packages/tools/ozon-revenue-per-product.js) реализован по плану (11 tasks через `superpowers:subagent-driven-development`): 4 pure-функции под TDD (18 unit-тестов), live Finance + FBS/FBO Posting API, pluggable formatters (console/json/xlsx все ✅ на smoke), `.bookva.cmd` + `.slovo.cmd`. README + bump `@books/tools` 0.3.2 → 0.4.0. Bonus fix: `xlsx.js` formatter null-cell crash (shared между analyst tools). Smoke Bookva 2026-05-01..02: 118 postings, take rate 37.76%. Wiki sync. **Локально на master, не push'ено.**_
_Updated: 2026-05-14 (вечер) — заведена `ozon-revenue-per-product` 🔴 active. CLI-tool в `packages/tools/`: за период по Ozon-магазину live из API считает net/gross/avg на проданный товар + take rate + FBS/FBO breakdown. Per-channel — `.cmd`-обёртки (паттерн репо). Spec одобрен: [.wiki/concepts/ozon-revenue-per-product-design.md](../.wiki/concepts/ozon-revenue-per-product-design.md). Не БД, не MCP, не `analyst/` (Finance API нет в БД — gap #1 [[db-analytics-baseline]]; user явно хочет live). Next: implementation plan через `superpowers:writing-plans`._
_Updated: 2026-05-14 (день) — `slovo-long-sale-promo` 🟢 **DONE end-to-end** ✅. Cohort1 (67) + cohort2 (168) = 235 SKU добавлены, activate отработал, **235/235 applied + 0 failed** (после retry'я 1 SKU). По следам — 2 bag в `ozon-promotion-activate.js` занесены в [business-logic-bugs](business-logic-bugs.md): (1) MinPrice clamp `9582fbe` пропущен в activate.js строка 155 (только в add-products.js); (2) `if (failed === 0)` строка 173 блокирует transition `scheduled→active` при любой failure. Workaround — ad-hoc `_retry-slovo-minprice.mjs` + manual SQL. Промо `id=2 long-sale-slovo` живая до 2026-05-27 14:00 UTC. Wiki sync pending._
_Updated: 2026-05-14 — заведена `slovo-long-sale-promo` 🟡 paused. Аналог bookva long-sale для канала Slovo (`id_sales_channel=2`). Воронка собрана по playbook'у [[ozon-promo-candidate-selection]]: cohort1 ≥60д+price∈[900,1500) → 67; cohort2 [30,60)д+price≥900 → 168; total **235 SKU** (bookva был 664, slovo меньше — 81% slovo IN_SALE дешевле 900₽). Акция `id=2 long-sale-slovo` создана в `scheduled`, -30%, до 2026-05-27. Pending: `ozon-promotion-add-products` обе партии + `ozon-promotion-activate`. Кандидаты в `tmp-designs/slovo-long-sale/`._
_Updated: 2026-05-13 (поздний вечер) — `ozon-promotions-rename-and-rekey` **DONE locally (awaiting push)** ✅. Скоуп расширили в процессе: помимо `ozon_` префикса + drop `id_product` + `snapshot_min_price`, переехали `promotions.id_seller` → `id_sales_channel` (FK к `sales_channels`). Ozon-creds остались на `sellers` (переезд на channels — отдельная задача). Migration 017 применена на проде, упала дважды (safe-update-mode + FK-prefix-index drop), оба fix зашиты в файл. 103/103 tools-tests green. Bumps @books/data 0.0.11, @books/tools 0.3.0. Live promo id=1 (12 SKU долгопрод) — данные сохранены через RENAME+UPDATE JOIN._
_Updated: 2026-05-13 (вечер) — заведена `ozon-promotions-rename-and-rekey` 🔴 active. После запуска live-promo выявлены 2 фундаментальных design-bug'а брейнсторма 12.05: (1) таблицы `promotions/promotion_products/promotion_price_snapshots` без префикса `ozon_` хотя фича строго про Ozon (конвенция books — `ozon_*` для Ozon-domain); (2) FK `id_product` к `books.products` для чисто Ozon-сущности (Ozon `offer_id` + `product_id` достаточны, FK создаёт ложную связь и заставляет резолвить `products.sku → offer_id` в активате). Fix: rename 3 таблиц на `ozon_*`, drop `id_product` FK, использовать `offer_id` как натуральный ключ, добавить `snapshot_min_price` для proper restore. Live promo (id=1, 12 SKU) сохраняется через RENAME TABLE. `domain-promotions-fix-real-ozon-wiring` 🟢 closed by commit afb423f._
_Updated: 2026-05-13 — заведена `domain-promotions-fix-real-ozon-wiring` 🔴 active. Pre-flight check для «12 SKU long-sale -30%» (12 товаров из `tmp-designs/long-sale-prices-result.json`, канал Bookva-Ozon, -30%, 2 недели) выявил блокер: все 4 CLI домен-промо шипнуты с stub'ами `importPrices: async()=>{}` и `getOzonPrice: async()=>({price:0,oldPrice:0})` — в Ozon ничего не уходит, snapshot=0, иллюзия успеха (БД пишется). Плюс bug в `promotion-add-products.js`: lookup `productSalesChannel` по `{idProduct}` ожидает поля `product_id`/`offer_id` отсутствующие в схеме `product_sales_channels`. `ozon_seller_products` устарел на 4 года (last update 2022-03-30) — не использовать. Решение: `products.sku == offer_id`, Ozon `product_id` через `productAPIGetProductInfoPrices(filter:{offer_id})`. Long-sale-promo заблокирована до починки._
_Updated: 2026-05-08 (ночь, prod-verified) — `docker-runner-env-decoupling` **CLOSED end-to-end** ✅ (scheduler 1.5.3 на проде, commit `bbbb428`). Восстановлен инвариант «добавить новый docker-tool = 0 правок volume default.json». `${config:KEY}` resolver в `dataToEnv(data, taskEnv, rootConfig)`, taskDef.env, `hashTask` включает env, `docker.containerEnv` удалён, добавлены `web.internalUrl`/`api.internalUrl` (внутренние имена для docker network proxy), PDF defaults 100×100mm в tasks.json (literal, не в default.json). 4 итерации релиза: 1.5.0 → 1.5.1 (internal split + ошибочно pdf section в default.json) → 1.5.2 (revert pdf section) → 1.5.3 (PDF 100×100). 21/21 unit-тест. Прод volume default.json обновлён юзером (api/web internalUrl, s3.bucket, docker engine settings, puppeteer.browserWSEndpoint = ws://chrome:3000?token=..., docker.containerEnv удалён). Smoke `createPickingListPdf` на проде успешен. Wiki sync ([sources/docker-runner-env-decoupling.md](../.wiki/sources/docker-runner-env-decoupling.md))._
_Updated: 2026-05-08 (ночь) — `docker-runner-env-decoupling` **CLOSED** ✅ (scheduler 1.5.0). Восстановлен инвариант «добавить новый docker-tool = 0 правок volume default.json». `${config:KEY}` resolver в `dataToEnv(data, taskEnv, rootConfig)`, taskDef получает поле `env { ... }`, `hashTask` теперь включает env, `docker.containerEnv` удалён. 12 новых unit-тестов (21/21 green). Wiki + log sync. **One-time prod migration** на VPS volume `/opt/books/job-scheduler/config/default.json` — pending push approval (см. .tasks/docker-runner-env-decoupling.md → "Resume protocol")._
_Updated: 2026-05-08 (поздний вечер) — `fbs-picking-list-pdf` end-to-end на проде ✅. Run 15814 (idPickingList=88): chrome render 21KB → S3 → api PATCH success, `pdf_remote_path` в БД. По ходу 5 hot-fix циклов: (1) sqlite3+cpu-features YN0009 на task-runner+web → root `dependenciesMeta.built=false`; (2) chrome WS 404 → restart 2-week-old browserless; (3) PATCH api fetch failed → API_URL `http://books-api:3021` internal; (4) [id].vue + [id]/print.vue Nuxt collision → rename в [id]/index.vue; (5) **WEB_URL puppeteer ходил на `books.kzntsv.site` который Traefik роутит на api**, исправлено на `http://books-web:3000`. По следам — заведена `docker-runner-env-decoupling` ⚪ (рефакторинг архитектуры env'ов docker-tools, делать в новой сессии). Pending: подобрать `PDF_HEIGHT` (юзер; default 177mm избыточен для 1-2 книг), full reserve-цепочка smoke (`fbsPickingReserve` с 2 attachments в email), wiki sync, close STATUS._
_Updated: 2026-05-08 (вечер) — `fbs-picking-list-pdf` Tasks 1-17 + wiki сделаны локально (13 commits на master, **не push'ено**). 5 версий bumped (web/api/tools/scheduler/task-runner). 27 unit-тестов (10 api + 12 tools + 8 waitForTaskRun + 7 spawnPdfAndAttachReport — все green). Деривации: `ctx.agenda` нет → `lib/agendaClient.js` singleton; puppeteer-core `^5.5.0` (workspace) вместо `^23`; tool kebab-case вместо camelCase. Pending user: (a) local smoke 4 сервисов; (b) push (project-discipline Rule 4 ask-mode); (c) prod migration 015; (d) prod containerEnv API_TOKEN через volume-mount; (e) prod trigger fbsPickingReserve verify._
_Updated: 2026-05-08 (день) — `fbs-picking-list-pdf` implementation plan расписан → [.tasks/fbs-picking-list-pdf-plan.md](fbs-picking-list-pdf-plan.md). 21 task, 7 phases (DB → print page → tool → scheduler config → chain → smoke → wiki). 3 open questions резолвлены: (1) image=`node:22-bookworm-slim` без chrome, переиспользуем remote chrome `wss://chrome.kzntsv.site` через `puppeteer.connect()`; (2) token-auth=query `?token=` через existing `02-auth.js` + bypass в `auth.global.ts` для print-роутов; (3) sync email-PDF=in-process polling `task_runs` (timeout 5min, на fail email с XLS-only + warning). Не новый workspace `pdf-generator/`, а tool в `packages/tools/` + matrix entry. DB-write через api `PATCH /fbs/pickingLists/:id/pdf`, не direct sequelize._
_Updated: 2026-05-08 — `scheduler-docker-runner` **CLOSED** ✅. Smoke testEcho на проде: run id=15456, status=success, log с 5×`[N/5] hello` + stderr `done after 5 iterations`, container_id non-null, --rm отработал, agendaJobId Bug#3 pattern. Phase 5 готов end-to-end. Инцидент по пути: Gitea API stuck → решился рестартом `gitea`+`gitea-db` (вики `concepts/gitea-stuck-recovery.md`). Native build deps (python3+make+g++) добавлены в scheduler/task-runner Dockerfiles. `fbs-picking-list-pdf` unblocked → 🔴 active. Total 27 commits в master (T1-T20 + 4 fix-коммита: yarn.lock, native build, summary)._
_Updated: 2026-05-07 (T20 wrap-up) — `scheduler-docker-runner` Phase 5 готов локально (20 commits, T1-T19 на master локально). Smoke тест `testEcho` ждёт push → build-tools.yml matrix → deploy. После verify закроется до ✅. `fbs-picking-list-pdf` 🟡 → 🔴 unblocked._
_Updated: 2026-05-07 (поздний вечер) — implementation plan для `scheduler-docker-runner` написан: [.tasks/scheduler-docker-runner-plan.md](scheduler-docker-runner-plan.md). 20 tasks с TDD red→green→commit, 4 pure-logic utils + handler с mocked dockerode + параметризованный Dockerfile + matrix workflow + smoke testEcho. Open questions резолвлены (pull retry 3x exp 5/15/45s, heartbeat 10s, smoke image bash:5.2 или dedicated). Next: запустить execution через `superpowers:subagent-driven-development` (recommended) или `superpowers:executing-plans`._
_Updated: 2026-05-07 (вечер) — после TDD-разнарядки и discovery что Phase 5 docker-runner — stub. Заведена `scheduler-docker-runner` 🔴 active (Phase 5 в [pluggable-scheduler](../.wiki/concepts/pluggable-scheduler.md), `handlers/docker.js` отсутствует). `fbs-picking-list-pdf` переведена в 🟡 paused — blocker на Phase 5. PDF теперь рендерится через docker-runner (новый workspace `packages/pdf-generator/`), task-runner image остаётся чистым. TDD: Phase 5 — TDD целиком (Q2/Q3/Q4); PDF — композит (visual/oneshot/wrapper для UI/migration/Dockerfile + TDD для pdf-generator core/auth/reserve-цепочки/email-mod)._
_Updated: 2026-05-07 — заведена `fbs-picking-list-pdf` 🔴 active. PDF-версия листа подбора рядом с XLS в reserve-цепочке, карточки по местам, портрет A4, mobile-first для сборки с телефона при RKN-блокировке web. Спека утверждена: [.wiki/concepts/fbs-picking-list-pdf.md](../.wiki/concepts/fbs-picking-list-pdf.md). Implementation plan ещё не написан._
_Updated: 2026-05-04 — заведена `sync-prices-ozon-to-yandex` 🔴 active. Tool в `packages/tools/` который тянет live цены с указанного Ozon-канала и проставляет в YM-канал (1:1 на поле `price`, `old_price` генерится по legacy-алгоритму `generateOldPrices.js`). Hybrid CLI+module + handler-обёртка в одном файле; thin re-export stub в `task-runner/tasks/` для auto-discovery реестра. tasks.json entry без cron (manual trigger). Дизайн одобрен (brainstorming sesh), код в работе. Также: paтern `packages/task-runner/tasks/` объявлен устаревшим — новые handler-обёртки в `packages/tools/`.
_Updated: 2026-05-03 — `analyst-report-formats` **CLOSED** ✅. Реализованы 5 pluggable formatters (console/md/xlsx/html/json), CLI-dispatcher, output-path helper, 11 unit тестов. Wiki обновлена. Хук yarn.lock улучшен (проверяет dependencies/devDependencies, не весь package.json). Push в master (commit b6007f8). Pattern готов для первых analyst tools (staff-performance / listings-vs-orders / sales-seasonality).
_Updated: 2026-05-01 (поздний вечер²) — `books-analyst-mcp` **CLOSED целиком**. Phase 1.3 (`books-data-mcp`) отменён архитектурно: вместо stdio MCP-сервера над `@books/data` аналитика будет писаться как обычные JS-файлы в `packages/tools/analyst/<name>.js` (паттерн hybrid CLI+module — соседи `ozon-transactions-analysis.js`, `ozon-products-price-analysis.js`). LLM работает в `books/`, sequelize-модели открываются `Read`-инструментом напрямую → MCP-stdio + ssh-tunnel + saved queries — лишний слой. План `docs/superpowers/plans/2026-04-30-books-data-mcp.md` помечен OBSOLETE первой строкой. Phase 1.4 (analyst-каталог) отменён вместе с 1.3. Phase 1.2 (yandex-market-mcp) остаётся отдельной задачей на полке. Решение в [.wiki/concepts/analyst-tooling.md](../.wiki/concepts/analyst-tooling.md)._
_Updated: 2026-05-01 (поздний вечер) — заведена `ozon-knowledge-base-ingest` 🟡 paused. Цель — слить базу знаний `seller-edu.ozon.ru/libra` (комиссии/тарифы + цены/акции + дайджест) в `.wiki/raw/seller-edu/` для LLM-аналитика. Эксперимент: Ozon Antibot палит CDP-протокол → markitdown/curl/playwright (headless/headful/stealth) — всё 403 либо "Доступ ограничен". Юзер параллельно saved страницу через **obsidian-web-clipper** → референсный MD (290 KB из 369 KB HTML, таблицы/якоря/frontmatter сохранены). **Архитектурный вывод:** clipper использует под капотом open-source `defuddle` (MIT, [github.com/kepano/defuddle](https://github.com/kepano/defuddle)) — node.js библиотека для clean content extract + turndown. Финальная архитектура задачи: **A3 (playwright CDP-attach к НЕ-playwright Chrome юзера)** для HTML acquisition + **defuddle npm** для HTML→MD = byte-совместимо с тем что clipper делает руками. Pipeline сделать в `packages/seller-edu-ingest/`. Artefacts/scaffold в [tmp-designs/seller-edu-experiment/](../tmp-designs/seller-edu-experiment/). Memory: `reference_ozon_seller_edu_antibot`._
_Updated: 2026-05-01 (вечер) — `books-analyst-mcp` Phase 1.1 ✅ closed. Real-API smoke 11/11 на кабинете bookva/буква (10 OK + reviews.list 403 как ожидалось без Premium). В smoke'е вылез баг handler-обёртки SDK request'а (все 11 tools посылали пустой body, Ozon возвращал 400 «filter required»); пофикшен двумя коммитами: `1af4fcf` (wrapper показывает HTTP body в error text, bump 0.0.4) → `de3360c` (handler-ы переключены на плоский request, bump 0.0.5; покрыто `handler-body.test.js` 10 кейсов через `Configuration.fetchApi`-перехват). Wiki + log + decision log синхронизированы. Phase 1.2 (YM-MCP), 1.3 (books-data-mcp), 1.4 (analyst-wiring) — отложены, реактивация по запросу._
_Updated: 2026-05-01 (день) — `books-analyst-mcp` 🔴 active. Scope Phase 1.1 сужен до **только `ozon-mcp`** (юзер: «набъём шишки на Озон, Яндекс потом будет легче»). 11 read-only tools, multi-shop через env-vars, SDK 0.0.30 покрывает все 3 TODO плана (Returns/Analytics/Reviews) реальными классами. Документация — через npm-mcp + результат в `.wiki/packages/ozon-mcp.md`. Plan: [docs/superpowers/plans/2026-04-30-ozon-mcp.md](../docs/superpowers/plans/2026-04-30-ozon-mcp.md). Per-task: [books-analyst-mcp.md](books-analyst-mcp.md). YM-MCP / data-mcp / analyst-wiring — отложены за Phase 1.1._
_Updated: 2026-05-01 (утро) — `books-ops-mcp` **Phase 2 CLOSED**. 4 mariadb tools (query/describe + scheduler.recentRuns/failedRuns) на проде с APP_VERSION=master-d426d03. Smoke 7/7 green; `failedRuns(since='7d')` вытащил 8 known bugs из business-logic-bugs end-to-end. CI был сломан днём (502 Bad Gateway → runner+gitea restart + workflow_dispatch unblock; recovery pattern в memory `reference_gitea_ci_stuck_recovery`). Spec+plan: `docs/superpowers/{specs,plans}/2026-04-30-books-ops-mcp-phase2*`. Wiki `packages/ops-mcp.md` расширена per-phase секцией. Skill `.claude/skills/books-ops-mcp/SKILL.md` обновлён на 12 tools._
_Updated: 2026-04-30 (поздний вечер ²) — `ntfy` Bug #1 fix запушен (pending verify): `gitea.run_id``gitea.run_number` в `RUN_URL` обоих workflow ([build.yml](../.gitea/workflows/build.yml), [deploy.yml](../.gitea/workflows/deploy.yml)). Root cause — Gitea UI route `/repo/actions/runs/{run}` парсит `{run}` как `runIndex` (per-repo sequential = `run_number`), а не глобальный `run.ID`. Build URL «работал» (HTTP 200, но открывал чужой run), deploy URL — 404 (run_id выше суммарного count). Verify: `git commit --allow-empty -m "verify ntfy URL" && git push` → tap ссылки в обоих ntfy-каналах. После verify закрыть `ntfy` или открыть Phase 3 (disk-alert)._
_Updated: 2026-04-30 (поздний вечер) — закрыты `registry-gc` (Phase 1+2+3+5 на проде, цикл retention работает на двух фронтах Gitea+VDS) и `switch-npm-mcp-to-local-stdio` (`.mcp.json` переведён с docker на локальный node — убирает «MCP unavailable» при daemon/volume-mount issue'ах). `ntfy` переведена в 🟡 paused — Phase 1+2+4 на проде, замечен баг #1 (битая ссылка на Gitea в deploy-уведомлении, в build-уведомлении ОК) — не блокер, ждёт возврата._
_Updated: 2026-04-30 (вечер close) — `books-ops-mcp` **Phase 1 CLOSED** ✅. 8 tools на проде, hot-fix `agendaState` (aggregate pipeline) задеплоен и verified end-to-end. Skill, wiki, business-logic-bugs обновлены. Phase 2 (recentRuns/failedRuns через MariaDB) не делаем без конкретного триггера. Юзер закрыл досрочно (без 24h soak) — все evidence для close уже были._
_Updated: 2026-04-30 (вечер) — `books-ops-mcp` Phase 1 на проде закрыт по фактическим возможностям. После Phase 1 deploy первый же вызов `ops.scheduler.agendaState` показал output 95 KB (превышал Claude cap). Hot-fix `5b56dc3`: aggregate pipeline по name → 23 ряда (~9 KB), 33/33 unit-теста, push'ен и задеплоен (APP_VERSION=master-5b56dc3 в env). Все 8 tools работают на проде. По следам прохода — 4 бага бизнес-логики занесены в business-logic-bugs (commit `86be892`). Создан project-scoped skill `.claude/skills/books-ops-mcp/SKILL.md` (commit `4470ce1`) чтобы Claude не лез через ssh за тем, что покрыто tools. Wiki-страница [@books/ops-mcp](../.wiki/packages/ops-mcp.md) добавлена._
_Updated: 2026-04-30 (день) — scheduler два hot-fix'а пушены (`0d5ffd6` + `b6dcdfc`): (1) `productsIndexSyncronization` legacy zombie-handler убран из `tasks.json` после disable=true в Mongo; (2) **критический**: cron http-jobs не выполнялись из-за idempotency check на shared Mongo `_id` — все ~20 cron'ов годами не sync'ились (sync остатков!), fix `agendaJobId = ${_id}:${Date.now()}` в http-handler. После recreate'а scheduler юзер подтвердил «задвигался». Также написаны spec + 4 плана для `books-analyst` MCP-стека (`docs/superpowers/{specs,plans}/2026-04-30-*`), но **юзер поставил `books-ops-mcp` ВЫШЕ analyst в приоритете** — для ускорения фиксов и развязывания рук в SSH._
_Updated: 2026-04-29 — registry-gc Phase 5 (VDS-side `vdsDockerCleanup` через `books-docker-proxy`) committed локально, ждёт user-go на push. Юзер уточнил: Gitea — 5TB пофиг, горит на VDS. Phase 1+2+3 (Gitea side) ранее запушены. poll-pending-ozon-imports Phase 1-5 запушена, pending Phase 6 verify._
_Updated: 2026-04-28 — заведён scheduler-ui-improvements (имя модуля в логе + copy-all-info кнопка). business-logic-bugs пополнен fbsPickingReserve `recipients: []` SMTP-вспышкой + закрыт loadProductsToWarehouse double-run (fix локально). Заведён scheduler-bugs (404 на detail-страницах тасков из-за отсутствующего tasks.json в api image, запушен). Заведён scheduler-core-replace (концепт замены Agenda на свой core, idea-only). agenda-admin-ui CLOSED. Заведена npx-runner-pattern (документирована, не реализована)._
## 🟢 [move-promo-bookva-to-slovo](move-promo-bookva-to-slovo.md) — DONE 2026-05-21 — свёрнут Bookva долгопрод, перенесён на Slovo (-30% до 04.06)
**Status:** done
**Outcome:** Bookva promo id=1 (663 SKU active, -30%) → ended + restored. Slovo promo id=2 (235 SKU active, -30%) → ended + restored (превентивно, чтобы snapshot copy не утянул slovo-цены в новой id=3). Создана slovo-promo id=3 `long-sale-slovo-2026-05-21`, -30%, до 2026-06-04 14:00 MSK, 660/660 applied. По пути найден 3-й bug в `ozon-promotion-deactivate.js` (defensive expected=snapshot вместо action) — обход через oneshot `_push-prices-from-snapshot.mjs`. 3 SKU `19119894`, `Б2008714`, `Б607029` отсутствуют в Slovo каталоге (добавлены в Bookva после Pass 1 миграции) — не критично для текущей задачи. Bookva-каталог остался в продаже с нормальными ценами (по решению юзера). Артефакты прогона в `tmp-designs/bookva-cancel-to-slovo/`.
**Acceptance:** все 4 пункта задачи юзера ✅ (отменены акции в Букве с сохранением списка / цены в Слове = ценам Букве / создана акция в Слове на 2 недели / применена к 660 из 663).
**Blocker:**
**Branch:** `master` (никаких коммитов; БД-state на проде через ad-hoc oneshot)
---
## 🔴 [fbs-picking-list-pdf-thumbs](fbs-picking-list-pdf-thumbs.md) — bugfix: PDF листа подбора 38MB → SMTP 552; thumb-endpoint в api
**Status:** active
**Where I stopped:** написан task-файл, корень диагностирован (CDN full-res обложки puppeteer вшивает в PDF без даунсемпла); готов писать TDD red-tests для `thumbLib.js`.
**Next action:** написать `packages/api/server/routes/images/thumbLib.test.js` (parseThumbParams + isAllowedHost), убедиться что красные, затем impl + handler + sharp в package.json + swap src в `FbsPickingPlacePrint.vue`.
**Blocker:**
**Branch:** `master` (commits локально, push после verify)
---
## 🟢 [slovo-product-creation-recovery](slovo-product-creation-recovery.md) — DONE 2026-05-17 — Pass 3 + Pass 4 для снятия is_created=false блокеров
**Status:** done (awaiting push approval)
**Outcome:** Реализованы 2 новых CLI согласно concept'у [ozon-product-creation-blockers](../.wiki/concepts/ozon-product-creation-blockers.md). **Pass 3** `products-fix-creation-errors-from-mirror.js` (TDD 10/10, pure helpers `chunkArray` + `extractErrorAttrIdsByOfferId(items, codesWhitelist)`, reused `filterCopyableAttrs` из Pass 2): Slovo GetProductList(ALL) → GetProductInfoList → парсит `attribute_id` из `errors[]` по whitelist (default `CONDITIONAL_ATTRIBUTE_ERROR`) → Bookva matching → GetProductAttributesV4 → UpdateAttributes Slovo чанками 100. **Pass 4** `products-reimport-stuck.js` (TDD 8/8, pure helpers `chunkArray` + `isStuckCreating(item)`): auto-detect Slovo карточек с `is_created=false`+пустой `errors[]` → Bookva IN_SALE match (silent skip без master) → ImportProductsBySKU **без diff'а**. **Prod smoke apply:** Pass 3 task_id=4479563894 **74/74 imported** (из 169 targeted / 102 Bookva matched), Pass 4 task_id=4479600569 **6/6 imported** (из 632 stuck / 626 без Bookva master). Bump `@books/tools` 0.9.0→0.10.0 MINOR (+2 bin/exports). README +2 секции. Wiki concept updated. **185/185 tests green** (+18). Ожидаемый эффект: 80 карточек после next cron syncStocksWithWarehouse (каждые 8ч) попадут в IN_SALE.
**Acceptance:** все 7 пунктов task-файла ✅.
**Blocker:**
**Branch:** `master` (5 коммитов локально, **не push'ено** — project-discipline Rule 4 ask-mode)
---
## 🟢 [slovo-in-sale-gap-diagnose](slovo-in-sale-gap-diagnose.md) — DONE 2026-05-16 — diag tool + два root cause fix (delay + filter) + Pass 1 #2
**Status:** done
**Outcome:** Diagnostic CLI `analyst/ozon-in-sale-gap.js` (TDD 13/13: chunkArray + bucketizeInfoItem с priority chain archived > out_of_stock > no_price > failed > state_failed > moderate > status > state) + `.cmd`-обёртка + pluggable formatters через `runFormat`. Root cause #1: **`sellers.put_on_sale_delay_days`** для Slovo=7 vs Bookva=0 → 433 свежих книг с `put_on_sale_date >=2026-05-09` не попадали в available SQL → stock=0. Юзер поставил delay=0. Manual `syncStocksWithWarehouse(idSeller=2)` +194 IN_SALE (4548→4742). Root cause #2: фильтр legacy У/Б2/К в Pass 1+2 был слишком широкий, юзер: «только У не переносим». Filter обновлён. Bump `@books/tools` 0.6.0→0.8.0 (0.7.0 — diag tool MINOR, 0.8.0 — filter change MINOR). Pass 1 #2 на проде: 444 кандидата (включая Б2/К), 5 task_id, 0 unmatched. Прогноз через 1-3 дня после модерации: Slovo ~5180 IN_SALE (~95.6% от Bookva 5418). Локально 1 коммит pending push.
**Acceptance:** все 7 пунктов из task-файла ✅ (CLI создан / pipeline 5 шагов / bucket-разбивка / .cmd-обёртка / README / bump MINOR / prod smoke с numbers).
**Blocker:**
**Branch:** `master`
---
## 🟢 [ozon-products-copy-bukva-to-slovo](ozon-products-copy-bukva-to-slovo.md) — DONE 2026-05-15 — Bookva → Slovo: массовая привязка по SKU + докачка контент-рейтинга
**Status:** done (Phase 1+2 prod-shipped; ожидает user-go на push)
**Outcome:** Pass 1 — `products-import-by-sku.js` extended (CLI-flags, IN_SALE pivot), prod 608 IN_SALE-кандидатов в Slovo через 7 task_id, 0 дубликатов. Pass 2 — новый CLI `products-fill-improve-attributes.js` (TDD 13/13, pure helpers chunkArray/buildImproveIdsByOfferId/filterCopyableAttrs), prod smoke `--limit 100``task_id=4463148527` → 31/31 `imported` (verified GetImportProductsInfo, 10/31 имеют пост-импортные модерационные warnings — исходные проблемы каталога). `@books/tools` 0.5.1 → 0.6.0 MINOR. 4 Pass 2 commits локально (`77a1acf`, `56d5150`, `03b0e88`, `c5ae6e7`) — **не push'ено**, pending user-go. SDK discovery закрыл open вопросы 3-4 (chunks 100/1000/100, dry-run console-table); скорректирована спека §pipeline §8 (UpdateAttributes возвращает только `{task_id}`, per-item через GetImportProductsInfo).
**Next action:** user-go на push origin/master. Дальнейшая батч-докачка — повторные `--limit 100` (3825 ост. кандидатов).
**Spec:** [.wiki/concepts/ozon-products-copy-bukva-to-slovo.md](../.wiki/concepts/ozon-products-copy-bukva-to-slovo.md)
**Acceptance:** Pass 1 — **10/10 ✅**. Pass 2 — **9/9 ✅**.
---
## 🟢 [ozon-revenue-per-product](ozon-revenue-per-product.md) — DONE 2026-05-14 — выручка с проданного товара по магазину Ozon
**Status:** done
**Outcome:** CLI-tool `packages/tools/ozon-revenue-per-product.js` + `.bookva.cmd` + `.slovo.cmd`. 4 pure-функции под TDD (18 тестов). Live Finance + FBS/FBO API, pluggable formatters. Smoke на Bookva ✅ (console/json/xlsx). README. `@books/tools` 0.4.0. Bonus fix shared `xlsx.js` formatter.
**Blocker:**
**Branch:** `master`
---
## 🟢 [slovo-long-sale-promo](slovo-long-sale-promo.md) — DONE 2026-05-14 — аналог bookva long-sale для slovo (id=2)
**Status:** done
**Outcome:** End-to-end. Воронка собрана (cohort1 67 + cohort2 168 = 235 SKU), оба add-products пробежали без fail. Activate упал на 1 SKU (139258946) с MinPrice violation → ad-hoc retry script + manual SQL transition → **235/235 applied, 0 failed**. Промо `id=2 long-sale-slovo` `active`, -30%, до 2026-05-27 14:00 UTC. По следам — 2 bag в [business-logic-bugs](business-logic-bugs.md) (activate.js: clamp пропущен + status transition блокируется failure). Кандидаты + retry-log в `tmp-designs/slovo-long-sale/`. Deactivate в календарь на 2026-05-27.
---
---
## ✅ [scheduler-docker-runner](scheduler-docker-runner.md) — CLOSED 2026-05-08 — Phase 5 в pluggable-scheduler `runner: 'docker'`
**Status:** closed
**Outcome:** **End-to-end на проде.** 4 pure-logic utils + handler.js (full lifecycle: pull-on-miss → semaphore → attach+demux+ring-buffer → wait+timeout/SIGTERM/SIGKILL → mapExit → finalize → remove always) + 12 unit-тестов + параметризованный `packages/tools/Dockerfile` (`BASE_IMAGE`+`BIN_NAME`) + matrix `build-tools.yml`. **Smoke testEcho:** run id=15456, status=success, container_id non-null, log = `[1/5] hello`×5 + stderr `done after 5 iterations`, --rm отработал, agendaJobId `${_id}:${ts}` Bug#3 pattern. По пути решены: yarn.lock drift (YN0028), native build deps в alpine (python3+make+g++), volume-mount sync default.json на VPS, инцидент Gitea API stuck (рестарт `gitea`+`gitea-db`, вики обновлена). Bumped `@books/job-scheduler` 1.2.3→1.3.0.
---
## 🟢 [ozon-promotions-rename-and-rekey](ozon-promotions-rename-and-rekey.md) — fix 3 design proeb'а: ozon_ prefix + drop id_product + id_seller→id_sales_channel
**Status:** done (awaiting push) — 103/103 tools tests green, migration 017 применена на prod, live promo id=1 (12 SKU) данные сохранены
**Where I stopped:** 2026-05-13 вечер. Scope расширили в процессе: помимо `ozon_` prefix + drop `id_product` + `snapshot_min_price`, переехали `promotions.id_seller``id_sales_channel` (FK к sales_channels). Ozon-creds остались на sellers — отдельная задача. Migration упала на проде дважды (safe-update-mode + FK-prefix-index) — оба fix зашиты в файл.
**Acceptance:** (1) ✅ migration 017 + RENAME 3 таблиц, drop id_product, id_seller→id_sales_channel, snapshot_min_price; (2) ✅ 3 sequelize моделей переименованы, FK ассоциация на salesChannel; (3) ✅ 5 CLI переименованы, --product-ids→--offer-ids, workaround через books.products.sku удалён (probe Ozon by offer_id); (4) ✅ _lib/ozon-promotions/ + side-effects через salesChannel.getSeller(); (5) ✅ tests 103/103 (26 lib + 77 CLI); (6) ✅ bumps @books/data 0.0.10→0.0.11, @books/tools 0.2.2→0.3.0.
**Blocker:**
**Branch:** `master`
---
## ✅ [domain-promotions-fix-real-ozon-wiring](domain-promotions-fix-real-ozon-wiring.md) — CLOSED 2026-05-13 — real Ozon SDK + min_price + 5 багов
**Status:** closed (commits 89f4522 + afb423f, not pushed)
**Outcome:** Подключены real Ozon SDK вызовы к 5 domain-promotion CLI. Фикс через несколько итераций после pre-flight check'а на live-promo «long-sale -30% / 12 SKU»: (1) stub `importPrices/getOzonPrice` → real (`createOzonSideEffects`); (2) bug `productSalesChannel` lookup → `Product.findByPk → sku → Ozon API`; (3) `@books/data` пропустил регистрацию 3 promotion-моделей — добавлены; (4) `applyPriceToOzon` throws на failure (не глотает в results[].success=false); (5) `min_price` default = price (Ozon отвергал push ниже min_price); (6) `ozon-side-effects._unwrap` достаёт body из `ResponseError`. **Live promo `id=1` применена** через recovery oneshot — 12/12 цен -30% в Ozon верифицировано через `ozon_prices_list`. 58 unit-тестов green. Bump @books/tools 0.2.0→0.2.2, @books/data 0.0.9→0.0.10. Замечен в processе фундаментальный design defect — заведена follow-up `ozon-promotions-rename-and-rekey`.
---
## 🟡 [fbs-picking-list-pdf](fbs-picking-list-pdf.md) — PDF-версия листа подбора в reserve-цепочке (auto в email рядом с XLS)
**Status:** implementation done (Tasks 1-17 + wiki), awaiting prod verify
**Where I stopped:** 2026-05-08 вечер. Tasks 1-17 + Task 20 (wiki) сделаны, 13 commits локально. 27 unit-тестов green. 5 версий bumped. Не push'ено.
**Next action:** (a) локально запустить 4 сервиса (api/web/scheduler/task-runner) и сделать end-to-end smoke (открыть `/fbs/picking-lists/:id/print?token=<token>` в браузере, проверить что layout без navbar, page-break per place; trigger `fbsPickingReserve` через UI, ждать `[pdf]` записей в task-runner консоли, ждать email с двумя attachment'ами); (b) **разрешить push** в master — auto-build+deploy всех 4 стеков через Gitea Actions; (c) на проде apply миграцию 015 (`packages/data/migrations/015-add-pdf-remote-path.sql`, idempotent); (d) на проде в scheduler default.json через volume-mount выставить `docker.containerEnv.API_TOKEN` реальным значением (текущий placeholder `<SET_IN_PROD_VOLUME_MOUNT>`); (e) trigger fbsPickingReserve на проде, verify `pdf_remote_path` заполнено + email с двумя attachment'ами.
**TDD:** компонент'ы по-разному покрыты. Q3 (api endpoint Q3 identifier security) — TDD pure-logic helpers (10 тестов). Q2/Q3 (tool puppeteer/S3/api PATCH) — 12 unit-тестов с mocked deps. Q4 (waitForTaskRun + spawnPdfAndAttachReport branching) — 15 unit-тестов. Visual/oneshot/wrapper — `[skip-tdd: …]` маркеры в commits.
**Blocker:**
**Branch:** `master` (13 commits локально, awaiting push approval)
---
## 🟡 [sync-prices-ozon-to-yandex](sync-prices-ozon-to-yandex.md) — копир цен Ozon→YM (`packages/tools/`)
**Status:** paused — Phase 1 закоммичен, smoke OK, real apply упёрся в YM rate-limit на ~37 chunk'е.
**Where I stopped:** 2026-05-04. Local apply (dryRun=false) на каналах 1→3: первые 37 chunks (~18500 цен) ушли успешно, остальные ~164 chunks (~81 800 SKU) упали в `ym-update-failed` с generic «Response returned an error code» (SDK прячет body). Diag-probe одиночными запросами те же sku — оба `200 OK`, значит payload и ключ валидные → причина похожа на YM rate-limit/quota windowing. **Side effect: первые ~18500 SKU в YM-кабинете Bookva уже имеют новые цены + новые рандомные `discountBase`** (sku 100001+ из верха списка по id_product). Не страшно — repeated apply просто перезапишет, цены идемпотентны.
**Next action:** (a) добавить fetchApi-middleware в `sync-prices-ozon-to-yandex.js` который читает body на не-2xx и кидает с status+body (по аналогии с `de3360c` для Ozon SDK); (b) тротлинг между chunks (1-2 sec) + retry-on-429 с backoff 30-60s (сейчас 2/4/8/16/32 — мало для YM-окон); (c) опционально — chunk size <500. После (a) re-probe одного из failed chunk'ов чтобы увидеть конкретный YM-error → root cause → fix → re-apply.
**Blocker:**
**Branch:** `master`
---
## 🟡 [ozon-knowledge-base-ingest](ozon-knowledge-base-ingest.md) — слить seller-edu.ozon.ru/libra в .wiki/raw/seller-edu/
**Status:** paused — архитектура **A3 + defuddle** выбрана, не реализована. Сотни страниц вручную не вариант; defuddle (open-source MIT, движок obsidian-web-clipper) даёт референсное MD, playwright `connectOverCDP` к настоящему Chrome юзера обходит anti-bot.
**Where I stopped:** 2026-05-01 поздний вечер. Эксперимент: markitdown solo / curl / playwright headless / headful / stealth — всё 403 либо "Доступ ограничен" (Ozon палит CDP-fingerprint). Юзер параллельно вручную saved страницу через obsidian-web-clipper — MD-качество референсное, **показал что defuddle (=движок clipper'а) — путь**. Финальная архитектура: A3 (CDP-attach к НЕ-playwright Chrome) для HTML acquisition + defuddle npm для HTML→MD + frontmatter писать самим. Артефакты эксперимента в [tmp-designs/seller-edu-experiment/](../tmp-designs/seller-edu-experiment/), пример качественного MD — [.wiki/raw/2026-05-01-вознаграждения-...md](../.wiki/raw/). Лессон в memory `reference_ozon_seller_edu_antibot`.
**Next action:** реализовать pipeline (см. шаги 1-9 в task-файле). Prereq — юзеру настроить Chrome с `--remote-debugging-port=9222` постоянно. Pipeline: discovery sitemap → defuddle pipeline test на 1 URL → seed комиссии/тарифы (throttle 8s/req, ≤30/сессия) → seed цены/акции → дайджест-cron в job-scheduler. Параллельно — проверить публичный Telegram-канал Озон как резервный канал дайджеста.
**Blocker:**
**Branch:** `master`
---
## ✅ [pluggable-scheduler](pluggable-scheduler.md) — CLOSED 2026-04-28 (+ Phase 9 + private S3 fix 2026-04-28)
**Status:** closed
**Outcome:** Phase 1-7 + Phase 9 в проде. 23 http-таска в [tasks.json](../packages/job-scheduler/config/tasks.json) с `label`/`description`/`params` для админки. Pure reconciler (`config.jobs[]`+`lib/jobs/` удалены), все таски рендерят markdown-отчёт + always-upload в S3 (`reportS3Key` контракт после 2026-04-28 — bucket private, api-proxy с `requireAdmin`), `task_runs` self-cleanup. Mongo-флаг `_fromYml` мигрирован на `_fromConfig` в startup-миграции. Playbook `docs/playbooks/pluggable-scheduler.md` обновлён + ALS log-capture как Phase 5b + грабли 21-25.
---
## ✅ [agenda-admin-ui](agenda-admin-ui.md) — CLOSED 2026-04-28
**Status:** closed
**Outcome:** Все 5 фаз + post-launch polish + все 4 deferred follow-up закрыты в одной сессии. Раскатано в `74254ea`. Главное: ALS console-capture в `task_runs.log` (ctrl-через-Z для дебага), markdown-отчёты через api-proxy на приватный S3 (старый MinIO Browser UI больше не вылезет), human-readable filename при скачивании, RunsTable порядок+ссылки, UPagination, sales-channels автоподсказки. Wiki синхронизирована: новая страница [Task log capture](../.wiki/concepts/task-log-capture.md), обновлены [Task-runner handlers](../.wiki/concepts/task-runner-handlers.md) и [SalesChannel](../.wiki/entities/sales-channel.md).
---
## 🟡 [poll-pending-ozon-imports](poll-pending-ozon-imports.md) — split loadProductsToOzon на submit + cron poll
**Status:** implementation done, awaiting prod verify
**Where I stopped:** Phase 1-5 + wiki сделаны локально (resume 2026-04-29). Tool/handler/poll-job/tasks.json/UI polling/api no-cache/wiki — всё в индексе. Push готов; следующий шаг — push в master, дождаться auto-deploy всех 4 стеков, manual trigger main run в проде, через 5-10 мин проверить что poll-job tick прошёл и mutate'ил result.
**Next action:** push → wait deploy → trigger main run на проде → verify (см. Resume protocol в task-файле).
**Blocker:**
**Branch:** `master`
---
## ✅ [docker-runner-env-decoupling](docker-runner-env-decoupling.md) — CLOSED 2026-05-08 — расщеплены infra-creds и task-contract
**Status:** closed (implementation done, локально, awaiting push approval + one-time prod volume migration)
**Outcome:** Восстановлен инвариант «**добавить новый docker-tool = 0 правок volume default.json scheduler-а**». Реализован syntax `${config:KEY}` (string-form, partial interpolation supported) в `dataToEnv(data, taskEnv, rootConfig)`. taskDef получил поле `env { ... }` в tasks.json. `default.json` причесан: `docker.containerEnv` удалён, появились `api.{url,authToken}`, `web.url`, `s3.bucket`. `hashTask` включает env → правка блока в tasks.json триггерит reconcile. 12 новых unit-тестов (21/21 green: `dataToEnv: ${config:KEY}` whole-string / partial / multiple refs / literal / unknown ref throws / data+taskEnv merge / numeric/boolean stringify / null skipped / undefined arg + handler-level taskDef.env merged into Env / no-env passthrough). Bumped `@books/job-scheduler` 1.4.0→1.5.0 [MINOR]. Wiki [scheduler-docker-runner](../.wiki/concepts/scheduler-docker-runner.md) обновлён, decision-record в [log.md](../.wiki/log.md). **One-time prod migration** на VPS volume `/opt/books/job-scheduler/config/default.json` — добавить секции `api.{url,authToken}`, `web.url`, `s3.bucket`, удалить блок `docker.containerEnv`. После — никаких правок при добавлении следующих docker-tools.
---
## ⚪ [npx-runner-pattern](npx-runner-pattern.md) — npx-таски как третий способ запуска (без third runner-type)
**Status:** ready
**Where I stopped:** Phase 5c в [pluggable-scheduler playbook'е](../docs/playbooks/pluggable-scheduler.md) задокументирован (2026-04-28). Два пути: A) handler+`spawn('npx',...)` для http-runner-а, B) docker с `image: node:N` + `command: ['npx',...]`. В коде ничего ещё нет.
**Next action:** при первом реальном npx-кандидате (например `audit-ci` weekly или `npm-check-updates`) — Phase 1: reference-handler `runNpxCommand.js`. Phase 2 (docker `command` override) откладывается до имплементации docker-runner-а.
**Blocker:**
**Branch:**
---
## 🟡 [scheduler-bugs](scheduler-bugs.md) — реестр багов /scheduler UI и /admin/scheduler/* api
**Status:** 2 hot-fix'а пушены 30.04, awaiting verify (юзер подтвердил «задвигался»), real-fix rewrite позже
**Where I stopped:**
- **Bug #1** (2026-04-28): 404 на `/admin/scheduler/tasks/<name>` — open / fixing.
- **Bug #2** (2026-04-30): `productsIndexSyncronization` legacy handler с зомби-локами. **Mitigated** через `disabled:true` в Mongo + удалён task definition из `tasks.json` (`5c17135`). Real fix (rewrite или delete handler) — отдельной задачей.
- **Bug #3** (2026-04-30): **критический** — cron http-jobs не выполнялись годами. `handlers/http.js:14` шлёт `String(job.attrs._id)` как `x-agenda-job-id`. Для cron через `agenda.every()` это shared id → task-runner `findByAgendaJobId` находит старый run от первого dispatch'а → возвращает 200 без создания нового → handler не выполняется. Все ~20 cron http-handler'ов (sync остатков, postings, prices…) thinks работают (Mongo `lastFin` обновляется), реально не делают ничего. **Fix:** `agendaJobId = ${_id}:${Date.now()}` (`ddbbffd`, push `b6dcdfc`). Юзер 30.04 ~04:00 МСК подтвердил «задвигался шедулер».
**Next action:** наблюдать стабильность 24h. Если возникнут новые failures — копать. Real rewrite `productsIndexSyncronization` — отдельной задачей или delete (если не нужен).
**Blocker:**
**Branch:** `master`
---
## ✅ [books-ops-mcp](books-ops-mcp.md) — CLOSED 2026-05-01 (Phase 1+2: 12 read-only tools для prod-диагностики)
**Status:** closed
**Outcome:** **12 tools на проде** (8 Phase 1 + 4 Phase 2). Phase 1 (`5b56dc3`, 30.04): 4 docker + 2 mongo + 2 scheduler-mongo. Phase 2 (`d426d03`, 01.05): `mariadb.query/describe` (generic) + `scheduler.recentRuns/failedRuns` (narrow). Mongo + MariaDB через docker network `proxy` (172.20.0.0/16), без exposing ports. **Defence-in-depth** Phase 1 (3 слоя: proxy-ro POST→403, mongo whitelist, env mask) + Phase 2 (6 слоёв: ops_ro@'172.%' GRANT SELECT, multipleStatements:false, checkSql regex+ALLOWED_STARTS, enforceLimit force LIMIT, query timeout, capPayload 1MB cap). 3 unit-tested helpers (TDD) + integration tests via docker-compose + smoke 7/7 на проде. Один review-fix (comment-strip в enforceLimit) и один hot-fix `failedRuns` (prod-схема: status='error' + колонка `error`, не 'failed'/fail_reason). Wiki [@books/ops-mcp](../.wiki/packages/ops-mcp.md) расширена per-phase. Skill `.claude/skills/books-ops-mcp/SKILL.md` обновлён на 12 tools. **Phase 3** (write tools под двойным confirm: unlockAgendaJob, disableAgendaJob) — только при 2-3 повторяющихся ручных операциях такого класса.
---
## ✅ [books-analyst-mcp](books-analyst-mcp.md) — CLOSED 2026-05-01 (поздний вечер²) — целиком
**Status:** closed (Phase 1.1 на проде; Phase 1.3+1.4 отменены архитектурно; Phase 1.2 — на полке)
**Outcome:** `@books/ozon-mcp@0.0.5` — 11 read-only tools на проде (Phase 1.1, `ozon-seller-api-client@0.0.30`, multi-shop bookva/слово, smoke 11/11). **Phase 1.3 (`books-data-mcp`) отменён 2026-05-01 (поздний вечер²)**: brainstorming показал — MCP-stdio + ssh-tunnel + saved queries в `*.sql` лишний слой, LLM работает в `books/` напрямую и `packages/tools/` уже задаёт паттерн hybrid CLI+module (соседи `ozon-transactions-analysis.js`, `ozon-products-price-analysis.js`). Аналитика теперь пишется как обычные JS-файлы в `packages/tools/analyst/<name>.js` по запросу, без отдельной инфраструктуры. План `docs/superpowers/plans/2026-04-30-books-data-mcp.md` помечен OBSOLETE первой строкой. Phase 1.4 (analyst-каталог) отменён вместе с 1.3 — отдельный каталог не нужен. Phase 1.2 (`yandex-market-mcp`) — план остаётся отдельной задачей, реактивация по запросу. Решение зафиксировано в [.wiki/concepts/analyst-tooling.md](../.wiki/concepts/analyst-tooling.md).
---
## ✅ [analyst-report-formats](analyst-report-formats.md) — CLOSED 2026-05-03 — pluggable formatters для analyst tools
**Status:** closed
**Outcome:** Реализован pattern pluggable formatters для аналитических CLI-tools. **5 formatters:** console (stdout, console.table), json (stdout), md (GFM таблицы + chart placeholder), xlsx (ExcelJS, sheets per table, bold headers, auto-width), html (HTML table + vega-embed, CDN default + --standandalone). **Infrastructure:** `_lib/cli-format.js` (dispatcher с --format/--out/--standalone), `_lib/output-path.js` (timestamped paths в `tmp-designs/analyst-reports/`), `_test/` (11 unit тестов). **Wiki:** обновлены `analyst-tooling.md` (секция "Output formats"), новая страница `tools-analyst.md`, `index.md` + `log.md` entries. **Хук yarn.lock улучшен:** теперь проверяет dependencies/devDependencies изменения, не весь package.json (ложные срабатывания при scripts changes устранены). Push в master (commit b6007f8). Тестовые кейсы (staff-performance, listings-vs-orders, sales-seasonality) — отдельными задачами по мере возникновения.
---
## 🟡 [scheduler-ui-improvements](scheduler-ui-improvements.md) — UX-идеи для /scheduler админки
**Status:** paused
**Where I stopped:** заведено 2026-04-28. Две идеи: (1) имя исполняемого модуля в логе (через stack-walk на каждую console-строку при `data.debug=true`); (2) кнопка «Copy all» на /scheduler/runs/[id] которая собирает status+params+log+result в plain-text для вставки Claude'у в чат.
**Next action:** реализуется при первом серьёзном дебаг-сценарии. Низкий приоритет.
**Blocker:**
**Branch:**
---
## ⚪ [scheduler-core-replace](scheduler-core-replace.md) — заменить Agenda+Mongo на свой core поверх MariaDB
**Status:** idea (not started)
**Where I stopped:** 2026-04-28 концептуальное обсуждение pros/cons/подводных камней. Crash recovery был преувеличен мной как блокер — это `setInterval` + `UPDATE WHERE locked_at < NOW() - X` ~50 LOC. Реальные сложности: lock TTL trade-off (handler в task-runner отдельным процессом), idempotency на retry, catch-up policy. Middle-ground — тонкая обёртка `cron-parser` + MariaDB `scheduled_tasks` ~300 LOC.
**Next action:** не трогать. Триггеры пересмотра: `@agendajs/mongo-backend` ломается на npm-bump, нужны priority/dependencies/per-task concurrency, или желание снять Mongo-контейнер с VPS.
**Blocker:** soak Phase 7 pluggable-scheduler ещё свежий — преждевременно ломать стабильное.
**Branch:**
---
## ⚪ [tools-architecture-revision](tools-architecture-revision.md) — ревизия границ packages/tools vs packages/task-runner/tasks
**Status:** ready (defined, не приоритет)
**Where I stopped:** задача заведена 2026-04-27 при дизайне `productSalesChannel`-sync — старая конвенция «tools атомарны, не лезут в БД» отменена (выбран X: tool пишет в БД сам). Юзер: «подходы нужно будет подвергнуть ревизии». Накопить 2-3 sync-таска, потом ревизировать pattern.
**Next action:** дождаться 2-3 примеров реализации, потом brainstorm границ + обновить `.wiki/packages/tools.md` и `concepts/task-runner-handlers.md`.
**Blocker:**
**Branch:**
---
## 🟡 [feedback-changes](feedback-changes.md) — фидбэк юзеров + изменения API маркетплейсов
**Status:** paused
**Where I stopped:** Все коммиты партии запушены (`46c20e3` upload polling, `6ae3a12` главная, `ebe1ebd` /jobs 500, `9d61850` web FBS-роуты, `ae9b6dd` иконки Pagination/SelectMenu). Юзер не приносил новой партии.
**Next action:** ждать фидбэка; в бэклоге — блок метрик дашборда + переименование API-роутов FBS (web уже переименован, API не тронут).
**Blocker:**
**Branch:** `master`
---
## 🟡 [pwa](pwa.md) — installable PWA для @books/web
**Status:** paused
**Where I stopped:** spec (`2a48230`) + impl (`b0a6820`) + task-wiki (`7a28f91`) запушены. На проде раскатано.
**Next action:** ручная проверка на устройствах — iPhone Safari (Add to Home Screen), Android Chrome (install prompt), Lighthouse PWA audit. По итогам — закрыть или завести follow-up.
**Blocker:**
**Branch:** `master`
---
## 🟡 [web-design-system](web-design-system.md) — единая дизайн-система @books/web
**Status:** paused
**Where I stopped:** Большая итерация UX/UI запушена (lucide bundle вместо CDN, мобильный navbar с hamburger, ProductSkuLink + useTabTarget, /fbs/orders как сетка карточек, USwitch вместо чекбокса в /fbs/summary, hover-open dropdown через UPopover, scrollbar-gutter:stable, spinner-overlay, реордер orders header, label «Канал продаж», SKU+title pattern на /epz/search).
**Next action:** ждать новой партии фидбэка по UX.
**Blocker:**
**Branch:** `master`
---
## 🟡 [business-logic-bugs](business-logic-bugs.md) — реестр багов бизнес-логики
**Status:** paused
**Where I stopped:** bug #2 (Ozon labels portrait, `09962bd`) и bug #3 (upload-history badge, `0aae243` + `4358806`) закрыты. bug #1 (`ozonFbsProduct` notNull, `2940123`) запушен — ждёт прод-логи task-runner на пропадание `notNull Violation`.
**Next action:** при возврате — проверить прод-логи task-runner после раскатки 4 локальных коммитов (заодно с bug #1 verification). Либо ждать новых багов.
**Blocker:**
**Branch:** `master`
---
## 🟡 [vps-migration](vps-migration.md) — переезд CentOS 7 → Debian 12
**Status:** paused
**Where I stopped:** план в [concepts/vps-os-migration](../.wiki/concepts/vps-os-migration.md), оценить сроки.
**Next action:** при готовности — оценить сроки и blast radius.
**Blocker:**
**Branch:** `master`
---
## ✅ [registry-gc](registry-gc.md) — CLOSED 2026-04-30 (Gitea-side retention + VDS-side prune)
**Status:** closed
**Outcome:** Полный цикл retention в проде на двух фронтах. Gitea-side: `mode=min` build cache + `registryGc` cron `0 4 * * 0` MSK (keep last 3 versions per package) + Gitea built-in `[cron.cleanup_packages] OLDER_THAN=24h` (blob compaction). VDS-side: `vdsDockerCleanup` cron `0 5 * * *` MSK через `books-docker-proxy` (4 stage prune containers/dangling/old-images/buildkit-cache, volumes намеренно не трогаем). Notify в `books-alerts` через ntfy summary после каждого run. Phase 4 (disk-pressure alert) опциональный — открыть отдельной задачей при надобности.
---
## 🟡 [ntfy](ntfy.md) — push-уведомления (Telegram заблокирован РКН)
**Status:** paused — Phase 1+2+4 на проде; Bug #1 fix запушен 30.04 поздний вечер², pending verify.
**Where I stopped:** 2026-04-30 поздний вечер². Bug #1 (Click-URL → 404 в `books-deploys`, чужой run в `books-builds`) — root cause: Gitea UI route `/repo/actions/runs/{run}` парсит `{run}` как `runIndex` (per-repo seq = `gitea.run_number`), а не `gitea.run_id`. Поправлены оба workflow ([build.yml](../.gitea/workflows/build.yml) env `RUN_URL`, [deploy.yml](../.gitea/workflows/deploy.yml) env `RUN_URL`).
**Next action:** push верификационный коммит `git commit --allow-empty -m "verify ntfy URL" && git push` → дождаться build+deploy → tap ссылки в обоих ntfy-каналах, должен открыться ИМЕННО smoke run сверху списка. После verify — закрыть task или начать Phase 3 (disk-alert).
**Blocker:**
**Branch:** `master`
---
## 🟡 [playbook](playbook.md) — оперативный playbook для инцидентов VPS / API
**Status:** paused (scope сужен — реюзабельные плейбуки выехали)
**Where I stopped:** scope-расщепление 2026-04-27: реюзабельные «как сделать» плейбуки (CI/CD, scheduler) живут в `docs/playbooks/` и закрыты. Эта задача теперь только про **incident playbooks** (что делать когда сломалось) — не начато.
**Next action:** структурировать сценарии (API down, registry full, certificate expiry), оформить в `docs/runbooks/` или `docs/playbooks/incidents/`.
**Blocker:**
**Branch:** `master`
## ✅ [switch-npm-mcp-to-local-stdio] — DONE 2026-04-30 — переключён `npm-mcp` в `.mcp.json` с docker на локальный node stdio. Контекст и обоснование — в общей вики: `concepts/local-stdio-mcp-deploy` (там же таблица альтернатив B/C/D на случай, если разделяемый конфиг пакетов не подойдёт для books).
Текущий блок в `books/.mcp.json` (entry `npm-mcp`):
```json
"npm-mcp": {
"type": "stdio",
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "c:/Users/vitya/projects/npm-mcp/packages.config.json:/app/packages.config.json:ro",
"-v", "npm-mcp-cache:/data",
"registry.kzntsv.site/npm-mcp:latest"
]
}
```
Заменить на:
```json
"npm-mcp": {
"type": "stdio",
"command": "node",
"args": ["C:/Users/vitya/projects/npm-mcp/build/index.js"]
}
```
Прочие записи (`context7`, `books-ops`) **не трогать**. Путь к `build/index.js` указан под Windows-машину; на другой машине, если books открывается там — подставить локальный путь к клону `npm-mcp` (или замаппить через user-scope `claude mcp add --scope local`, вариант B из общей вики, чтобы не коммитить машинно-специфичный путь).
Решение принято в сессии npm-mcp 2026-04-30: docker-путь периодически даёт «MCP unavailable» (daemon, volume mounts на Windows), а сервер уже скомпилирован локально и реестр доступен с хоста — Docker не даёт ничего, кроме лишней точки отказа.
**Status:** done 2026-04-30
**Outcome:** В `books/.mcp.json` entry `npm-mcp` переключён с `docker run` на `node C:/Users/vitya/projects/npm-mcp/build/index.js`. Прочие записи (`context7`, `books-ops`) не трогали. Юзеру: после следующего рестарта Claude Code в books — проверить что `mcp__npm-mcp__*` инструменты доступны и `search_docs` отвечает. Если открываем books на другой машине — подставить локальный путь к клону `npm-mcp` (или вариант B из общей вики `concepts/local-stdio-mcp-deploy` — user-scope mcp add, чтобы не коммитить машинно-специфичный путь).
**Branch:** `master`
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: npm-mcp / 2026-04-30T12:49:35.450Z -->
---
## ⚪ [refresh-project-bootstrap] — Run the `project-bootstrap` skill in this repo to refresh its layout to the canonical state (git, .gitignore, README.md, .wiki/, .tasks/, CLAUDE.md). The skill handles both greenfield bootstrap and refresh of existing repos.
**Status:** ready
**Where I stopped:** (not started)
**Next action:** Invoke the `project-bootstrap` skill at the repo root, confirm each file write, push to main.
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: projects-meta-mcp / 2026-05-01T09:02:43.955Z -->
---
## 🟢 [bootstrap-upgrade-pilot] — CLOSED 2026-05-07 — `project-bootstrap` v1.10.0 upgrade pilot успешен
**Status:** done
**Outcome:** Bootstrap v1.10.0 идемпотентно добавил 3 новых триггера в `CLAUDE.md`: `follow tdd-criteria`, `delegate to interns when allowed`, `recommend, don't menu`. Существующие строки нетронуты, пользовательские комментарии (первые 6 строк) сохранены. **Идемпотентность подтверждена** — повторный прогон находит 0 missing triggers. Skill-deps check (Step 5.6) корректно валидирует наличие всех 11 fulfiller'ов на хосте. Commit `5ca16d9`. Массовая раскатка на остальные проекты — следуя приоритету: `_meta` (координатор) → `claude-skills``npm-mcp``books` (уже done) → остальные.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: meeting-room / 2026-05-07T06:30:35.846Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-07T06:54:14.493Z / note: Пилот успешен: после прогона `upgrade project` v1.10.0 в `books/CLAUDE.md` строки 13-18 содержат все 6 канонических триггеров. Идемпотентность подтверждена. Аудит остальных делегирован интерну. Следующий шаг — координатор в `_meta` + 11 per-project тасок blocked-by-coordinator. -->
---
## 🟢 [domain-promotions-pointers] — CLOSED 2026-05-12 — Bootstrap-pointers для design `domain-promotions`. Секция "Domain conventions" добавлена в `.wiki/CLAUDE.md`.
**Status:** done
**Outcome:** В `.wiki/CLAUDE.md` добавлена секция "Domain conventions" с блоком "Mandatory: read design context before implementation". Три источника в порядке: (1) global wiki `concepts/domain-promotions` через `mcp__projects-meta__knowledge_get`, (2) brainstorm trace `~/projects/.workshop/.archive/2026-05-12-books-domain-promotions.md`, (3) локальный `overview.md` для быстрой ориентации. Следующие агенты будут знать где лежит полный design context а не угадывать из `where_stopped` one-liner'ов.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:29:18.799Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-12T20:45:00.000Z / note: .wiki/CLAUDE.md обновлён, domain-promotions-schema разблокирован -->
---
## 🟢 [domain-promotions-schema] — CLOSED 2026-05-13 — Миграция + Sequelize-модели для доменных акций-скидок
**Status:** closed
**Outcome:** Migration 016 (`b732360`): три `CREATE TABLE IF NOT EXISTS` — promotions, promotion_products, promotion_price_snapshots. Event-based таймстампы в promotion_products (addedAt/appliedAt/restoredAt/removedAt/failedAt). Индексы: promotions(status, start_at), promotion_products(id_promotion, removed_at), promotion_price_snapshots(id_promotion, id_product) UNIQUE. FK: promotion→seller (CASCADE), product→promotion (CASCADE), snapshot→promotion+product (CASCADE). 3 sequelize-модели с camelCase/snake_case field mapping. `[skip-tdd: oneshot] [skip-tdd: wrapper]` — migration (Q7) + model declarations ≤10 significant lines (Q8). Smoke idempotency — pending prod apply.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:29:35.342Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit b732360 -->
---
## ✅ [domain-promotions-lib](domain-promotions-lib.md) — CLOSED 2026-05-13 — `f61c679` — 4 pure-logic модуля + 24/24 unit-теста green
**Status:** closed
**Outcome:** `resolve-cheapest-winner.js` (pure, 5 тестов), `snapshot-source.js` (thunk-injected, 4 теста), `defensive-restore.js` (tolerance-based, 7 тестов), `apply-price-to-ozon.js` (chunked+retry, 8 тестов). Pure ядро изолировано от I/O через thunk-инжекцию. CLI-таски импортируют через `import { resolveCheapestWinner } from './_lib/promotions/resolve-cheapest-winner.js'`. Разблокирует 5 CLI-таск.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:29:46.312Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit f61c679 -->
---
## ✅ [domain-promotions-create-cli](domain-promotions-create-cli.md) — CLOSED 2026-05-13 — `35a0308` — validation TDD 8/8 green + Commander wrapper
**Status:** closed
**Outcome:** `promotion-create.js` с гибридным export+CLI. Валидация: endAt>startAt (2 теста), discountPercent 0-100 (2 теста), ISO8601 даты (2 теста), seller exists (1 тест), успешное создание (1 тест). Commander обёртка `[skip-tdd: wrapper]`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:29:54.322Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit 35a0308 -->
---
## ✅ [domain-promotions-add-products-cli](domain-promotions-add-products-cli.md) — CLOSED 2026-05-13 — `2573454` — inline apply + idempotency 14/14 green
**Status:** closed
**Outcome:** `promotion-add-products.js`. Scheduled: товары добавляются с addedAt без appliedAt. Active: inline snapshot+resolve+apply через lib модули. Идемпотентность: повторный вызов skip'ает уже добавленные, воскрешает removed. Commander обёртка `[skip-tdd: wrapper]`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:30:05.059Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit 2573454 -->
---
## ✅ [domain-promotions-remove-products-cli](domain-promotions-remove-products-cli.md) — CLOSED 2026-05-13 — `9d4f2f4` — soft remove + defensive restore 7/7 green
**Status:** closed
**Outcome:** `promotion-remove-products.js`. Soft remove: запись остаётся с removedAt, без DELETE. Active+applied: inline defensive restore с cheapest-wins. Divergence не валит операцию. Commander обёртка `[skip-tdd: wrapper]`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:30:14.211Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit 9d4f2f4 -->
---
## ✅ [domain-promotions-activate-cli](domain-promotions-activate-cli.md) — CLOSED 2026-05-13 — `f68ff22` — scheduled→active + snapshot+apply 8/8 green
**Status:** closed
**Outcome:** `promotion-activate.js`. Status guard: только scheduled→active. На каждый товар: snapshot source → resolve cheapest-winner → apply price. Поддержка actionPriceOverride. API ошибка на одном товаре не валит остальных. Commander обёртка `[skip-tdd: wrapper]`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:30:21.450Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit f68ff22 -->
---
## ✅ [domain-promotions-deactivate-cli](domain-promotions-deactivate-cli.md) — CLOSED 2026-05-13 — `31fde95` — active→ended + defensive restore 7/7 green
**Status:** closed
**Outcome:** `promotion-deactivate.js`. Status guard: active→ended, не ended/cancelled. Каждый applied товар: resolve cheapest-winner (self-excluding) → defensive restore (push/diverged). Pending товары: soft mark removedAt. Commander обёртка `[skip-tdd: wrapper]`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:30:29.476Z -->
<!-- closed-by: vitya@DESKTOP-NSEF0UK / 2026-05-13 / note: commit 31fde95 -->
---
## 🟢 [domain-promotions-review] — Code-review checkpoint для брейнсторма domain-promotions (промоушен 2026-05-12).
**Спецификация:** `concepts/domain-promotions` в global wiki (читать через `mcp__projects-meta__knowledge_get` slug=`concepts/domain-promotions`).
**Pre-impl bootstrap:** `domain-promotions-pointers` (заполнил `.wiki/CLAUDE.md` Domain conventions — без него review бы читал stub).
**Импл-таски (review против их acceptance criteria):** `domain-promotions-schema`, `domain-promotions-lib`, `domain-promotions-create-cli`, `domain-promotions-add-products-cli`, `domain-promotions-remove-products-cli`, `domain-promotions-activate-cli`, `domain-promotions-deactivate-cli`.
**Кто делает:** **не имплементер.** Следующая сессия в этом проекте (другая модель / другой день / другой агент) поднимает таску с чистым контекстом. «Я только что это написал» bias = главный риск.
**Чек-лист ревью:**
- Прочитать спецификацию (acceptance criteria каждой импл-таски).
- `git log --oneline` shipped-коммитов (по slug или scope в commit-message).
- Для каждой импл-таски: прогнать соответствующие тесты, реально проверить что они доходят до своих веток (не coverage-illusion).
- Сверить дизайн-decisions со shipped-кодом (signature, params, error-paths, безопасность).
- Особое внимание: cheapest-wins overlap (сценарии `A→B→A end→B end` и `A→B→B end→A end`), defensive-restore divergence detect, soft-remove семантика (`removedAt` без DELETE).
- Findings — отдельные follow-up tasks (`domain-promotions-<gap>-fix` или подобное) через `tasks_create`.
**Закрытие:** только когда все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note.
**Status:** done
**Where I stopped:** Review done 2026-05-13. 7 findings filed (3 critical, 3 medium, 1 low) → 4 follow-up tasks created: fix-snapshot-persistence, fix-status-timing, fix-removed-filter, fix-divergence-persist. 68/68 tests green but coverage illusion — production paths (snapshot write, Ozon API calls) not covered.
**Next action:** (none — kept until merged)
**Blocker:** bootstrap: domain-promotions-pointers; impl-tasks: domain-promotions-schema, domain-promotions-lib, domain-promotions-create-cli, domain-promotions-add-products-cli, domain-promotions-remove-products-cli, domain-promotions-activate-cli, domain-promotions-deactivate-cli
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-12T20:30:46.728Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-13T04:54:00.755Z / note: Review done 2026-05-13. 7 findings filed (3 critical, 3 medium, 1 low) → 4 follow-up tasks created: fix-snapshot-persistence, fix-status-timing, fix-removed-filter, fix-divergence-persist. 68/68 tests green but coverage illusion — production paths (snapshot write, Ozon API calls) not covered. -->
---
## 🟢 [domain-promotions-fix-snapshot-persistence] — Fix CRITICAL F1+F2+F3 from review: (1) idPromotionProduct→idPromotion+idProduct in snapshot lookup, (2) write PromotionPriceSnapshot.create() in activate+add-products inline, (3) pass product_id to defensiveRestore()
**Status:** done
**Where I stopped:** Fixed F1 (idPromotion+idProduct lookup instead of idPromotionProduct), F2 (PromotionPriceSnapshot.create in activate+add-products), F3 (product_id passed to defensiveRestore). 5 tests added (activate F2, deactivate F1+F3, add-products F2, remove-products F1+F3). 80/80 green.
**Next action:** (none — kept until merged)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-05-13T04:53:12.988Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-13T06:40:00.330Z / note: Fixed F1 (idPromotion+idProduct lookup instead of idPromotionProduct), F2 (PromotionPriceSnapshot.create in activate+add-products), F3 (product_id passed to defensiveRestore). 5 tests added (activate F2, deactivate F1+F3, add-products F2, remove-products F1+F3). 80/80 green. -->
---
## 🟢 [domain-promotions-fix-status-timing] — Fix MEDIUM F4 from review: move promo.status change AFTER processing products in activate+deactivate, not before
**Status:** done
**Where I stopped:** Fixed F4: status change moved after product processing loop with `if (failed === 0)` guard in both activate and deactivate. 2 tests added. 80/80 green.
**Next action:** (none — kept until merged)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-05-13T04:53:26.018Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-13T06:40:03.179Z / note: Fixed F4: status change moved after product processing loop with `if (failed === 0)` guard in both activate and deactivate. 2 tests added. 80/80 green. -->
---
## 🟢 [domain-promotions-fix-removed-filter] — Fix MEDIUM F5 from review: activatePromotion must filter removedAt IS NULL, per spec "Snapshot+apply всех removedAt IS NULL"
**Status:** done
**Where I stopped:** Fixed F5: added `removedAt: null` filter to PromotionProduct.findAll in activatePromotion. 1 test added verifying where clause. 80/80 green.
**Next action:** (none — kept until merged)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-05-13T04:53:28.201Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-13T06:40:07.371Z / note: Fixed F5: added `removedAt: null` filter to PromotionProduct.findAll in activatePromotion. 1 test added verifying where clause. 80/80 green. -->
---
## 🟢 [domain-promotions-fix-divergence-persist] — Fix LOW F6+F7 from review: (1) write divergedAt/divergedFrom/divergedTo to PromotionPriceSnapshot on divergence, (2) add integration tests covering production paths (snapshot write→find→restore→diverge)
**Status:** done
**Where I stopped:** Fixed F6+F7: divergence fields (divergedAt/divergedFrom/divergedTo) written to snapshot on divergence in both deactivate and remove-products. Integration test (F7) added covering full snapshot chain. 3 tests added. 80/80 green.
**Next action:** (none — kept until merged)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-05-13T04:53:30.748Z -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-05-13T06:40:10.665Z / note: Fixed F6+F7: divergence fields (divergedAt/divergedFrom/divergedTo) written to snapshot on divergence in both deactivate and remove-products. Integration test (F7) added covering full snapshot chain. 3 tests added. 80/80 green. -->
---
## 🟢 [analyst-workflow-claude-md] — DONE 2026-05-15 — секция Analyst workflow в корневом CLAUDE.md
**Status:** done
**Where I stopped:** Закоммичено `44d97b0`. Секция 39 строк после `we're on Windows`: триггер-фразы (выручка / средний чек / сезонность / staff perf / X by month / export / one-off), 3 шага (load `.wiki/concepts/` → check `packages/tools/{README,analyst/}` → write new), inline-блок стиля 11 пунктов (ESM, hybrid CLI+module, commander+isDirectRun, JSDoc, bin+exports, ozon/yandex client напрямую, sequelize+raw SQL, camel/snake mapping, `{meta, tables}` return shape, форматтеры `_lib/`, `.cmd`-обёртки), read-каналы (wiki/README/plans/sequelize), write-канал, anti-patterns (6 пунктов).
**Next action:** (none — kept until merged)
**Branch:** master (local, not pushed)
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-14T17:39:06.107Z -->
<!-- closed-by: vitya@local-windows / 2026-05-15 / note: commit 44d97b0 docs(claude-md). Размер 39 строк (спека 40-50, чуть короче — контент полный, паддинг ради счётчика не делал). Workshop archive не доступен (на DESKTOP-NSEF0UK), писал по acceptance criteria из task description. -->
---
## 🟢 [tools-ci-path-filter] — DONE 2026-05-15 (частично, спека ложна) — skip api/sched/tr build на CLI-only-tools diff
**Status:** done (narrowed scope)
**Where I stopped:** Закоммичено `102a92e` в [.gitea/workflows/build.yml](../.gitea/workflows/build.yml). Спека «исключить весь packages/tools» — ложная: `@books/tools/*` импортится в `packages/task-runner/tasks/*.js` (13+ файлов: fbs-ship-*, sync-prices-*, load-products-to-ozon, awaiting-packaging-*, sync-product-sales-channel-*, etc.) и в `packages/api/server/routes/fbs/*` (4 файла). Выпиливание `tools/` из `api_pat`/`sched_pat`/`tr_pat` целиком сломало бы prod runtime. Сужено до `tools_skip_pat='^packages/tools/(analyst|plans|not-for-qwen|output)/|^packages/tools/(README|QWEN)\.md$|^packages/tools/[^/]+\.cmd$'` — эти подпути проверены grep'ом на 0 импортов из runtime. Top-level `packages/tools/*.js` НЕ исключаем (там смесь runtime-imported и CLI-only). Test/lint job для tools — в `.gitea/workflows/*` отсутствуют вообще, спека приняла желаемое за сущее.
**Next action:** (none — kept until merged). Follow-up для расширения win: move analyst-CLI с top-level (ozon-revenue-per-product, ozon-transactions-analysis, ozon-products-price-analysis, archive-product, unarchive-product, products-import-by-sku, get-products-info-by-sku, update-drugs-blocked-products, ozon-promotion-*) в `analyst/` subdir.
**Branch:** master (local, not pushed)
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-14T17:39:13.960Z -->
<!-- closed-by: vitya@local-windows / 2026-05-15 / note: commit 102a92e ci(build). Реализация уже спеки: `analyst/`+docs+`.cmd` подпути skip api/web/sched/tr build; top-level *.js остаются под path-match потому что fbs-ship-*, sync-prices-*, load-products-to-ozon и т.п. реально импортятся в runtime. Юзер дал go на сужение варианта 1. -->
---
## 🔵 [analyst-workflow-review] — Review-checkpoint для брейнсторма books-analyst-workflow (промоушен 2026-05-14). Спецификация: workshop archive 2026-05-14-books-analyst-workflow.md. Impl-таски: analyst-workflow-claude-md, tools-ci-path-filter. Кто делает: НЕ имплементер. Следующая сессия с чистым контекстом (другая модель, другой день, другой агент) — бороться с bias я только что это написал. Чек-лист: 1) прочитать спецификацию из workshop archive; 2) проверить books CLAUDE.md секция Analyst workflow добавлена и читается на чистый старт сессии без двусмысленностей; 3) реально прогнать 2-3 триггер-фразы в свежей сессии (посчитай выручку, сезонность, отчёт) и убедиться что workflow корректно срабатывает: я читаю packages tools README/analyst до того как писать новый; 4) проверить CI: коммит strictly в packages tools не запускает deploy-job; обратное (коммит в packages api/data) триггерит как раньше; 5) finding ones — отдельные follow-up tasks через tasks_create. Закрытие: только когда все findings зафайлены или ревьюер подтвердил нет findings.
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** Дождаться зелёных blocker-тасок (analyst-workflow-claude-md и tools-ci-path-filter). Затем прогнать чек-лист (см. description). Findings — отдельные follow-up tasks через tasks_create.
**Blocker:** impl-tasks: analyst-workflow-claude-md, tools-ci-path-filter
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-14T17:39:18.934Z -->
---