# Doc2kb

> Converts a heterogeneous corpus of raw documents (PDF, DOCX, DOC, PPTX, IPYNB, RTF, MD, TXT, HTML, etc.) into a structured, LLM-optimized knowledge base — per-source Markdown + manifest.json + INDEX.md + AGENTS.md + a built-in BM25 search index (`query.sh`, citation-first), ready for ingestion in a separate Claude / Codex session. USE WHEN the user asks to ingest, index, preprocess, search, or bu…

- **Type:** Skill
- **Install:** `agentstack add skill-zevtos-agentpipe-doc2kb`
- **Verified:** Pending review
- **Seller:** [zevtos](https://agentstack.voostack.com/s/zevtos)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [zevtos](https://github.com/zevtos)
- **Source:** https://github.com/zevtos/agentpipe/tree/main/skills/doc2kb

## Install

```sh
agentstack add skill-zevtos-agentpipe-doc2kb
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# doc2kb — Document Corpus → LLM Knowledge Base

## ⛔ Правила, которые важнее всего остального

1. **NEVER summarize.** Контент сохраняется verbatim. Допустима только структурная очистка через `normalize_md.py` (дедупликация header/footer, whitespace, boilerplate-regex). Никакого rewriting, paraphrasing, перевода, "улучшения стиля". Пользователь хочет эквивалент того, что человек прочитал бы все файлы — потерянный при суммаризации факт не вернуть.
2. **NEVER silently skip a scanned PDF.** Если scout помечает PDF как `image_only` или `encrypted` — обязательно спросить пользователя одним сообщением (batch). См. `references/batch-questions.md`.
3. **NEVER bulk-extract без scout.** Сначала всегда фаза 2 (`scout_corpus.py`), потом фаза 3 (решения пользователя), и только потом фаза 4 (extract). Это нужно для оценки стоимости и для безопасного диалога с пользователем.
4. **NEVER touch binary files inside the kb output.** Картинки заменяются на placeholder (см. `extract_docx.py`), а не сохраняются как base64 в Markdown — base64-блобы катастрофически раздувают токены и бесполезны для LLM.
5. **NEVER bypass the venv.** Все скрипты запускаются через `ensure_env.py` (он находит venv в глобальном state-dir вне кода — ADR-008). Никогда не вызывайте extract-скрипты системным `python3` — зависимости не установятся в системный Python.

## When to use

Скилл триггерится, когда пользователь хочет:
- превратить папку с документами в knowledge base для Claude / Codex / другого LLM-агента;
- подготовить смешанный корпус (PDF + DOCX + PPTX + MD + …) к ingestion во второй сессии;
- получить per-source Markdown с manifest для последующего grep/read-навигатора;
- "обработать папку", "сделать базу знаний", "построить корпус", "feed files to Claude".

НЕ используй для:
- одиночных PDF операций (есть Anthropic'овский pre-built `pdf` skill — лучше для single-file);
- генерации новых документов (это `docx`/`pptx`/`xlsx` skills);
- RAG-векторизации с эмбеддингами (skill не строит vector store, только корпус для in-context-окна);
- кодовых репозиториев (используй `repomix` / `gitingest`).

## Workflow (5 phases)

**Canonical invocation pattern.** Every script in `/scripts/` is run through `ensure_env.py` as a wrapper. It handles venv bootstrap on first call (idempotent, ~30 ms on warm runs) and execs the target script inside the skill's `.venv`:

```bash
python3 /scripts/ensure_env.py  [args ...]
```

`` is the folder containing `SKILL.md` — typically `~/.claude/skills/doc2kb/` or `~/.codex/skills/doc2kb/`. Never invoke extract scripts directly with system `python3` — they import `_common.py` from the venv site-packages.

### Phase 1: Bootstrap (один раз)

```bash
python3 /scripts/ensure_env.py
```

(No target script → bootstrap only, prints venv-python path.) Creates the venv in a global state dir outside the code (ADR-008 — `$DOC2KB_HOME` or `${XDG_DATA_HOME:-~/.local/share}/agentpipe/doc2kb/venv`) and installs the lightweight tier: pymupdf4llm, pdfplumber, pypdf, pikepdf, python-magic, python-docx, mammoth, python-pptx, openpyxl, trafilatura, markdownify, charset-normalizer, striprtf, tiktoken.

**Системные зависимости (macOS):** `brew install libmagic` — обязательно, иначе python-magic не импортируется. На Linux: `apt install libmagic1`. На WSL то же. Без libmagic scout всё равно работает (fallback на расширение файла), но `mime_confidence` будет всегда `"high"` без перекрёстной проверки.

**Опциональная зависимость для DOCX с математикой:** `pandoc` (`brew install pandoc` / `apt install pandoc`). Если установлен, `extract_docx.py` автоматически переключается на него для документов, помеченных scout'ом как `has_equations: true`, и сохраняет OOXML math как `$...$` LaTeX. Без pandoc такие документы извлекаются через mammoth и теряют формулы (warning будет в JSON output). `pandoc` также используется как предпочтительный маршрут для **`.rtf`** (`extract_rtf.py`): он сохраняет таблицы/картинки/структуру. Без pandoc `.rtf` всё равно извлекается через pure-Python `striprtf` (plain text), так что rtf никогда не падает.

**Системный конвертер для legacy `.doc`:** бинарный формат `.doc` (OLE2) не читается чистым Python, поэтому `extract_doc.py` шеллится во внешний конвертер по убыванию точности: `soffice`/`libreoffice` (`brew install --cask libreoffice` / `apt install libreoffice-writer`) → конвертит в `.docx` и переиспользует весь DOCX-пайплайн (таблицы, картинки, OOXML math); на macOS — `textutil` (встроен) тем же путём; иначе `antiword` (`apt install antiword`) — только plain text. Ни один из конвертеров не ставится в venv (как и opt-in mineru CLI). Если ни одного нет на PATH, `extract_doc.py` выходит с кодом 2 и install-hint — главный цикл должен трактовать это как «нужна установка конвертера», а не как corrupt-файл, и залогировать в `_logs/errors.json`. Scout заранее предупреждает (`no .doc converter on PATH ...`), когда в корпусе есть `.doc`, а конвертера нет.

### Phase 2: Scout

```bash
python3 /scripts/ensure_env.py scout_corpus.py  
```

Производит `/_scout.json` с классификацией каждого файла. **Никогда не пропускайте эту фазу.** Schema файла зафиксирована в `references/format-spec.md`. Ключевые поля: `files[].extraction_strategy`, `files[].action_required`, `user_decisions_needed`.

**Опциональный флаг `--enable-mineru`.** Если установлен mineru tier (см. ниже «Optional heavy tier» / `references/mineru.md`), `scout_corpus.py --enable-mineru` автоматически роутит `image_only` PDF на extractor `mineru` вместо surfacing'а как `ask_user_ocr_strategy`. Без флага поведение не меняется — heavy ML deps никогда не активируются по-умолчанию.

### Phase 3: Decide

1. Прочитайте `/_scout.json`.
2. Если `user_decisions_needed` пуст — переходите к Phase 4.
3. Иначе — соберите **одно сообщение** пользователю по шаблону из `references/batch-questions.md`. Всегда батчите вопросы. Не задавайте по одному.

Возможные группы решений:
- `encrypted` — зашифрованные файлы (Office/PDF); опции: `password`, `skip`.
- `scanned_pdf` — image-only PDF; опции: `skip`, `ocr_tesseract`, `vlm_mlx`, `claude_pagewise`. *MVP поддерживает только `skip`.*
- `huge_file` — >50 MB или >500 страниц; опции: `skip`, `proceed`, `split`.
- `corrupt` — не открывается; опции: `skip`.
- `unsupported_format` — XLSX/EPUB/ODT/IMAGE (не в MVP); опции: `skip`. (`.doc` и `.rtf` теперь поддержаны — см. Phase 4.)

**Применение решений (важно для Phase 4).** Разрешив группу, обновите каждый файл в `_scout.json`: проставьте итоговый `extraction_strategy` (`skip` для отказа, либо рабочую стратегию для `proceed`) **и обнулите `action_required` (`null`)**. `extract_corpus.py` (Phase 4) откажется стартовать (exit 2), пока хоть у одного файла остался непустой `action_required` — это и есть гейт, гарантирующий, что Phase 3 пройдена.

### Phase 3.5: Apply overrides (опционально, вместо ручной правки `_scout.json`)

Чтобы прогнать пару файлов через другой extractor (например `mineru`) или
задать per-file настройки MinerU — **не редактируйте `_scout.json` руками**.
Опишите правила в `/_overrides.json` и примените их детерминированно:

```bash
python3 /scripts/ensure_env.py apply_overrides.py  [--dry-run]
```

```jsonc
{
  "version": 1,
  "overrides": [
    { "match": "papers/*.pdf",          // glob по source_path, ЛИБО точный doc-id, ЛИБО точный source_path
      "strategy": "mineru",
      "mineru": { "backend": "vlm-auto-engine", "lang": "cyrillic", "keep_raw": true },
      "popo": true },                    // per-file Popo opt-in/out (перекрывает env)
    { "match": "doc-012", "strategy": "skip" }
  ]
}
```

Гарантии: конфиг валидируется (неизвестный ключ / неверная `strategy` /
`backend` → exit 2, запись не происходит); `_scout.json` валидируется **после**
применения и пишется **атомарно** (`.tmp` → `os.replace`) — он никогда не
останется полузаписанным или со сломанной схемой. На runnable-стратегии
`action_required` обнуляется (override и есть Phase-3 решение). Правило,
совпавшее с **0 файлов**, — это ошибка (exit 1, типичная опечатка); `--dry-run`
показывает diff без записи; `--allow-unmatched` понижает 0-match до warning.
Блок `mineru` хранится структурно на записи файла — `extract_corpus.py` сам
рендерит из него безопасные CLI-флаги (никаких сырых arg-строк). Лупа после
этого: **scout → apply_overrides → (решить остаток) → extract_corpus**.

### Phase 4: Extract

**Запускайте один батч-диспетчер — не парсите файлы вручную.** `extract_corpus.py` читает `_scout.json` и сам прогоняет весь механический Phase-4 цикл: диспатчит каждую `extraction_strategy` на нужный extractor через `ensure_env.py`, пишет `docs/-.md`, копит `_logs/errors.json`, и печатает **один** JSON-summary последней строкой stdout. Это заменяет ручной цикл «построить команду → запустить → распарсить JSON → залогировать» по каждому файлу.

```bash
python3 /scripts/ensure_env.py extract_corpus.py 
# опции: --timeout 600 (на файл), --normalize (прогнать normalize_md после каждого), --quiet
```

Exit codes: `0` — все файлы дошли до терминального состояния (`needs_attention` это НЕ ошибка); `2` — отказ старта (нет `_scout.json`, либо у какого-то файла остался непустой `action_required` — вернитесь в Phase 3); `3` — был хотя бы один файл в `error`-бакете (см. `_logs/errors.json`).

Каждый файл попадает ровно в один бакет `counts`: `extracted` / `unchanged` (sha совпал, переэкстракция пропущена) / `skipped_by_decision` / `error` / `needs_attention` (= число `needs_install`). **Идемпотентность по `source_sha256`:** повторный запуск переэкстрактит только изменившиеся файлы — безопасно гонять много раз (например, после установки конвертера для `.doc`).

**Разберите `needs_attention[]` после диспетчера — это файлы, требующие ВАШЕГО суждения (диспетчер их НЕ решает сам, только surface'ит):**
- `reason: "needs_install"` — extractor вышел с кодом 2: `.doc` без системного конвертера, либо `mineru` CLI не установлен. `install_hint` подскажет, что поставить. Это НЕ ошибка и НЕ corrupt — поставьте инструмент и **перезапустите `extract_corpus.py`** (идемпотентность доделает только этот файл).
- `reason: "visual_transcription"` — `ok:true` PDF с warning'ом `mangled_visual_layout`: body извлечён, но позиционная математика рассыпана. Перечитайте исходный PDF через `Read` и перепишите body `docs/-*.md` вручную (см. pitfalls #13), затем `extraction_method: claude-pagewise-manual@1`.
- `reason: "dropped_pictures_residual"` — `ok:true` PDF с остаточными `dropped_pictures`: поле `pages` (список номеров страниц, восстановленный из тела документа) подскажет, какие страницы догнать через mineru page-patch (`extract_pdf_mineru.py --pages … --patch-into …`) или ручную транскрипцию.

Файлы `visual_transcription`/`dropped_pictures_residual` помечены `extracted_but_flagged: true` — считаются в `extracted` И присутствуют в `needs_attention[]` (body уже на диске, но требует доводки). `unclassified_warnings[]` эхо-ит любые нераспознанные warning'и дословно — **ничего не глотается молча**. После разбора `needs_attention[]` переходите к Phase 5 (`build_manifest.py` подхватит `_logs/errors.json`).

> Диспетчер использует таблицу стратегий ниже внутри себя. Прямой вызов одного extractor'а нужен только для адресных доводок (mineru page-patch, ручная переэкстракция одного файла):

| extraction_strategy | script |
|---|---|
| `pymupdf4llm`     | `extract_pdf_pymupdf4llm.py` |
| `mineru`          | `extract_pdf_mineru.py` *(opt-in tier, see below)* |
| `mammoth`         | `extract_docx.py` |
| `doc`             | `extract_doc.py` *(legacy `.doc`; needs system converter)* |
| `rtf`             | `extract_rtf.py` |
| `python-pptx`     | `extract_pptx.py` |
| `passthrough-md`  | `extract_md_txt.py --mode md` |
| `passthrough-txt` | `extract_md_txt.py --mode txt` |
| `trafilatura`     | `extract_html.py` |
| `ipynb`           | `extract_ipynb.py` |

```bash
python3 /scripts/ensure_env.py extract_pdf_pymupdf4llm.py \
    "" \
    "/docs/-.md" \
    --doc-id  \
    --source-rel ""
```

Каждый extract-скрипт пишет один `.md` в `/docs/` и возвращает JSON `{ok, out, tokens_estimated, warnings, ...}` в stdout (диспетчер парсит его за вас). `warnings` непустые означают, что extraction прошёл с deficiency (пустой результат, charts dropped, и т.д.).

**DOCX с математикой (автоматический pandoc-маршрут).** Если scout пометил DOCX как `has_equations: true` и `pandoc` есть на `PATH`, `extract_docx.py` автоматически переключается с mammoth на pandoc — он сохраняет OOXML math (``) как `$...$`/`$$...$$` LaTeX. Mammoth по-тихому дропает math элементы, и body после него ссылается на "формулу (1)", у которой нет содержимого. JSON `extractor` поле сообщит, какой маршрут был использован (`pandoc` или `mammoth+markdownify`). Если pandoc недоступен на машине с math-документом — будет warning с инструкцией установить (`brew install pandoc` / `apt install pandoc`).

**PDF с поломанными лигатурами `fi` / `ff` / `fl` (автоматическое восстановление).** pymupdf4llm ≤ 1.27.x теряет одну букву из ASCII-смаппленных лигатур в его spans→markdown сборке, давая `Ofcial` вместо `Official`, `fexible` вместо `flexible`, `trafc` вместо `traffic`, `Diffculty` вместо `Difficulty`, `quantifers` вместо `quantifiers`, и т.д. Raw `pymupdf.Page.get_text` отдаёт буквы корректно — баг локален в pymupdf4llm. `extract_pdf_pymupdf4llm.py` автоматически прогоняет `recover_ligatures` из `_common.py` на body и эмитит warning `ligatures_recovered: N word(s) ...` с количеством исправлений. `recover_ligatures` идемпотентен (повторный вызов даёт 0 правок), регистр первой буквы сохраняется (`Ofcial` → `Official`, `ofcial` → `official`). Если в новом корпусе встретится незнакомый broken pattern, эмитится дополнительный warning `ligature_residual: ...` с sample — расширьте `_LIGATURE_FIXES` в `scripts/_common.py`. Восстановление безопасно: lookbehind `(? picture [WxH] intentionally omitted /assets/`.
2. Заменяет плейсхолдеры на Markdown image links ``.
3. Подавляет `dropped_pictures` warning для тех плейсхолдеров, которые удалось заменить.

Дефолтное место для assets — `.parent.parent / "assets"`, что соответствует стандартному layout `/docs/*.md` → `/assets/`. Override: `--assets-dir ` и `--assets-rel `. Отключить: `--no-extract-images` (вернёт исходное поведение с loud warning).

**Warnings `mangled_visual_layout` / `dropped_pictures` (PDF only).** Это два варианта одной и той же поломки — PDF использует визуальный layout для математики (формулы набраны позиционно: дроби как стек символов, штрихи отдельными glyph'ами). pymupdf4llm не может это восстановить и либо рассыпает выражения в ``-цепочки одиночных символов внутри markdown-таблиц (`mangled_visual_layout`), либо выкидывает математические участки как `==> picture [WxH] intentionally omitted /scripts/ensure_env.py extract_pdf_mineru.py \
       "" "" \
       --doc-id  --source-rel "" \
       --pages "2,18-19,35,221,243-244,588" \
       --patch-into "/docs/.md" \
       --lang cyrillic
   ```

   Расценки на M-серии: ≈10 c/страница на vlm-mlx, то есть 9 страниц ≈
   полторы минуты. Frontmatter автоматически обновляется
   (`mineru_patched_pages: [...]`, `extraction_method_supplementary:
   mineru-vlm@x.y.z`), и ассеты для патчей сохраняются под именем
   `-page-mineru-imgN.` — pymupdf4llm-вые имена
   не затрагиваются. Установка tier и детали page-patching — в
   `references/mineru.md` (секция «Optional heavy tier» ниже — краткий обзор).

2. **Ручная транскрипция через Read tool (fallback).** Если mineru
   tier не установлен или его VLM не справляется (специфичные
   нотации, рукописные диаграммы):
   - Прочитайте исходный PDF напрямую через инструмент `Read` (Claude
     умеет читать PDF — рендерит страницы и видит математику
     визуально). Для уже извлечённых картинок в `assets/` Read тоже
     работает.
   - Перепишите body соответствующего `/docs/-*.md`
     вручную (или добавьте транскрипцию таблиц/формул из картинок
     рядом со ссылками), сохранив YAML frontmatter, но обновив:
     - `extraction_method: claude-pagewise-manual@1`
     - заменив warning

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [zevtos](https://github.com/zevtos)
- **Source:** [zevtos/agentpipe](https://github.com/zevtos/agentpipe)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-zevtos-agentpipe-doc2kb
- Seller: https://agentstack.voostack.com/s/zevtos
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
