Metadata-Version: 2.5
Name: nyshporka
Version: 0.6.3
Summary: Читання рукописних архівних справ і пошук прізвища в них — для генеалогів
Project-URL: Homepage, https://github.com/SERGIUSH-UA/nyshporka
Project-URL: Source, https://github.com/SERGIUSH-UA/nyshporka
Project-URL: Issues, https://github.com/SERGIUSH-UA/nyshporka/issues
Project-URL: Changelog, https://github.com/SERGIUSH-UA/nyshporka/blob/main/CHANGELOG.md
Author: Serhii Dalishchynskyi
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Keywords: archives,genealogy,handwriting,htr,kraken,ocr,parseq
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Ukrainian
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Topic :: Sociology :: Genealogy
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: loguru>=0.7
Requires-Dist: pillow>=10.4
Requires-Dist: platformdirs>=4.3
Requires-Dist: psutil>=6.0
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: pydantic>=2.9
Requires-Dist: pypdfium2>=4.30
Requires-Dist: python-frontmatter>=1.1
Requires-Dist: pyyaml>=6
Requires-Dist: rapidfuzz>=3.10
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: unidecode>=1.3
Provides-Extra: agent
Requires-Dist: anyio>=4.0; extra == 'agent'
Requires-Dist: mcp<2,>=1.2; extra == 'agent'
Provides-Extra: all
Requires-Dist: aiolimiter>=1.1; extra == 'all'
Requires-Dist: aiosqlite>=0.20; extra == 'all'
Requires-Dist: anyio>=4.0; extra == 'all'
Requires-Dist: boto3>=1.34; extra == 'all'
Requires-Dist: fastapi>=0.115; extra == 'all'
Requires-Dist: httpx[socks]>=0.27; extra == 'all'
Requires-Dist: keyring>=25; extra == 'all'
Requires-Dist: lxml>=5.3; extra == 'all'
Requires-Dist: mcp<2,>=1.2; extra == 'all'
Requires-Dist: openpyxl>=3.1; extra == 'all'
Requires-Dist: paramiko>=3.4; extra == 'all'
Requires-Dist: pdfplumber>=0.11; extra == 'all'
Requires-Dist: selectolax>=0.3.21; extra == 'all'
Requires-Dist: tenacity>=9; extra == 'all'
Requires-Dist: timm>=1.0; extra == 'all'
Requires-Dist: torch>=2.2; extra == 'all'
Requires-Dist: torchvision>=0.20; extra == 'all'
Requires-Dist: uvicorn[standard]>=0.32; extra == 'all'
Provides-Extra: app
Requires-Dist: fastapi>=0.115; extra == 'app'
Requires-Dist: uvicorn[standard]>=0.32; extra == 'app'
Provides-Extra: archives
Requires-Dist: aiolimiter>=1.1; extra == 'archives'
Requires-Dist: aiosqlite>=0.20; extra == 'archives'
Requires-Dist: httpx[socks]>=0.27; extra == 'archives'
Requires-Dist: keyring>=25; extra == 'archives'
Requires-Dist: lxml>=5.3; extra == 'archives'
Requires-Dist: pdfplumber>=0.11; extra == 'archives'
Requires-Dist: selectolax>=0.3.21; extra == 'archives'
Requires-Dist: tenacity>=9; extra == 'archives'
Provides-Extra: cloud
Requires-Dist: boto3>=1.34; extra == 'cloud'
Requires-Dist: paramiko>=3.4; extra == 'cloud'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Provides-Extra: htr
Requires-Dist: timm>=1.0; extra == 'htr'
Requires-Dist: torch>=2.2; extra == 'htr'
Requires-Dist: torchvision>=0.20; extra == 'htr'
Provides-Extra: ocr
Requires-Dist: paddleocr>=3.5; extra == 'ocr'
Provides-Extra: xlsx
Requires-Dist: openpyxl>=3.1; extra == 'xlsx'
Description-Content-Type: text/markdown

<p align="center">
  <img alt="Нишпорка"
       src="https://raw.githubusercontent.com/SERGIUSH-UA/nyshporka/main/src/nyshporka/brand/data/assets/mark.png"
       width="132" height="132">
</p>

<h1 align="center">Нишпорка</h1>

<p align="center"><em>Читає рукопис. Приносить знайдене.</em></p>

<p align="center">
  <a href="https://pypi.org/project/nyshporka/"><img alt="PyPI"
     src="https://img.shields.io/pypi/v/nyshporka"></a>
  <a href="https://github.com/SERGIUSH-UA/nyshporka/actions/workflows/ci.yml"><img alt="CI"
     src="https://github.com/SERGIUSH-UA/nyshporka/actions/workflows/ci.yml/badge.svg"></a>
  <img alt="Python" src="https://img.shields.io/pypi/pyversions/nyshporka">
  <img alt="Ліцензія" src="https://img.shields.io/badge/license-AGPL--3.0-informational">
</p>

Читання рукописних архівних справ і пошук прізвища в них — для генеалогів.

Комерційні OCR не читають скоропис XVIII–XIX ст., а платформи, які читають,
платні й не мають моделей під матеріал українських, молдовських і польських
архівів. Нишпорка закриває саме цю прогалину: беремо теку сканів із архіву й
отримуємо текст, у якому можна шукати прізвище.

> **Стан: alpha.** Каталоги, завантаження, читання рукопису, гортач, сховище
> прочитаного, пошук, браузерне обличчя й установлення працюють. Ваги трьох
> моделей ставляться командою `nysh models get`. Що саме не готове, у розділі
> «Чого ще немає» нижче, без замовчувань.

## Установлення

> 🤖 **Ставить агент?** Дайте йому одне посилання — [`AGENTS.md`](https://raw.githubusercontent.com/SERGIUSH-UA/nyshporka/main/AGENTS.md).
> Там і встановлення, і перші кроки, і межі, за якими вирішує людина.

### Windows — один файл

[**⬇ Завантажити Нишпорку**][setup] — запустити, відповісти на два питання,
натиснути «Встановити». Наприкінці майстра галочка «Запустити Нишпорку»
відкриє застосунок у браузері. Python, прав адміністратора й термінала не
треба: інсталятор приносить власний інтерпретатор і кладе все в профіль
користувача.

Довідники архівів їдуть усередині, тож `nysh find "моє село"` працює одразу.

⚠ **«Windows захистив ваш ПК»** — «Докладніше» → «Виконати в будь-якому разі».
Так Windows зустрічає кожну програму, яку ще мало хто завантажував; це
попередження зникає само, коли назбирається достатньо встановлень. Звірити
завантажене можна за файлом `.sha256` поруч із інсталятором у релізі.

[setup]: https://github.com/SERGIUSH-UA/nyshporka/releases/latest/download/nyshporka-setup.exe

### Термінал — усі системи

На чистій машині — **без Python і без прав адміністратора**, однією командою.
Клон репозиторію не потрібен: інсталятор приносить `uv`, `uv` приносить власний
інтерпретатор, усе лягає в профіль користувача.

```powershell
# Windows (PowerShell) — завантажити інсталятор і запустити
irm https://raw.githubusercontent.com/SERGIUSH-UA/nyshporka/main/install/windows.ps1 -OutFile "$env:TEMP\nysh-install.ps1"
powershell -ExecutionPolicy Bypass -File "$env:TEMP\nysh-install.ps1"
```
```sh
# Linux / macOS
curl -LsSf https://raw.githubusercontent.com/SERGIUSH-UA/nyshporka/main/install/unix.sh | sh
```

🔴 **Коли скрипт завершить роботу — закрийте вікно термінала й відкрийте нове.**
Доти команда `nysh` у ньому не знайдеться. Якщо й у новому вікні не
знаходиться — перезапустіть комп'ютер. (Тим, хто ставив інсталятором, це не
потрібно: ярлики працюють одразу.)

Якщо Python (або `uv`) на машині вже є, пакет ставиться з PyPI, і GitHub при
цьому не потрібен узагалі:

```bash
uv tool install "nyshporka[app,archives,htr]"   # або: pip install "nyshporka[app,archives,htr]"
nysh init                      # створити робочий простір
nysh doctor                    # перевірити те, що ламається тихо
nysh serve                     # відкрити застосунок у браузері
```

З клону репозиторію працюють ті самі скрипти:

```powershell
powershell -ExecutionPolicy Bypass -File install\windows.ps1
```
```sh
sh install/unix.sh
```

### Щоб читати рукопис — ще два кроки

🔴 **Установлений пакет сам по собі рукопис не читає.** Рушії живуть в окремому
інтерпретаторі поруч із простором, а ваги приходять окремим релізом — у колесі
їх немає навмисно, інакше кожен, хто прийшов подивитись каталог справ, платив
би за них гігабайтами. Без цих кроків `nysh read` відмовляє з поясненням, але
дізнатися про них посеред першої справи — найгірший момент.

```bash
nysh htr install    # середовище рушіїв: окремий інтерпретатор, kraken і PARSeq
nysh models get     # ваги трьох моделей, ~130 МБ, sha256 звіряється завжди
nysh doctor         # перевірка «Рушії читання» має стати зеленою
```

Обидва кроки — разові й лише для читання. Каталогам архівів, газетиру й пошуку
по описах вони не потрібні: набір `catalog` працює одразу після встановлення.

### Якщо ставить агент

Усе потрібне — у таблиці нижче. Ходити сторінками GitHub і клонувати репозиторій
не треба: адреси вище віддають сам скрипт, а PyPI-шлях обходиться без GitHub.

| завдання | команда |
|---|---|
| машина без Python | однорядковий інсталятор вище |
| Windows, без термінала | [`nyshporka-setup.exe`][setup] (тихо: `/VERYSILENT`) |
| Python або `uv` уже є | `uv tool install "nyshporka[app,archives,htr]"` |
| легкий набір, без `torch` (~2.5 ГБ) | `uv tool install "nyshporka[app,archives]"` |
| перевірити, що вийшло | `nysh doctor` |
| перелік операцій і їхні контракти | `nysh op <ім'я> --describe` |

#### Важелі

Скрипти лишаються повністю керованими — інсталятор `.exe` нічого з цього не
забрав, він лише інша обгортка навколо того самого `windows.ps1`.

| що задаємо | `windows.ps1` | `unix.sh` | `.exe` |
|---|---|---|---|
| набір | `-Preset catalog` | `NYSH_PRESET=catalog` | `/PRESET=catalog` |
| **склад пакета цілком** | `-Source 'nyshporka[app,archives]'` | `NYSH_SOURCE=…` | — |
| конкретна версія | `-Version 0.6.2` | (через `NYSH_SOURCE`) | вшита в файл |
| тека встановлення | `-Home_ D:\Nysh` | — | `/DIR=D:\Nysh` |
| не класти ярлик | `-NoLauncher` | (не кладе) | — |
| без питань | (їх і немає) | — | `/VERYSILENT` |
| журнал установлення | — | — | `/LOG=setup.log` |
| де жити дослідженню | `NYSHPORKA_WORKSPACE` | `NYSHPORKA_WORKSPACE` | `NYSHPORKA_WORKSPACE` |

`-Source` / `NYSH_SOURCE` перебивають набір і приймають будь-яку специфікацію
PEP 508 — саме ним береться нетиповий склад extras або конкретна версія
(`'nyshporka[app,archives]==0.6.0'`).

```powershell
powershell -ExecutionPolicy Bypass -File "$env:TEMP\nysh-install.ps1" -Preset catalog
```
```sh
curl -LsSf https://raw.githubusercontent.com/SERGIUSH-UA/nyshporka/main/install/unix.sh | NYSH_PRESET=catalog sh
```

Тихе встановлення інсталятором — для машин, де немає Python і не хочеться
розбиратися з політикою запуску скриптів:

```powershell
nyshporka-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /PRESET=researcher
```

⚠ **`irm … | iex` для цього скрипта не працює**, і відмова виглядає як десяток
помилок розбору в шапці, а не як зрозуміле повідомлення. Файл лежить із UTF-8 BOM (без нього Windows PowerShell 5.1 читає
кирилицю як ANSI й ламає мову ще до першого рядка виводу), а `Invoke-RestMethod`
віддає той BOM усередині рядка, де `iex` і `[scriptblock]::Create` його вже не
переживають. Тому спершу `-OutFile`, потім `-File`.

🔴 Після встановлення `nysh` доступний у **нових** вікнах термінала. Агентові,
який продовжує роботу в тому самому сеансі, простіше звертатись повним шляхом —
його друкує `uv tool dir --bin`. Після `.exe` той самий шлях лежить готовим у
`install-info.ini` в теці встановлення.

### Де лежить дослідження

Простір — тека, у якій живуть скани, прочитане й звіти. Типово `nysh init`
кладе її в `Документи/Нишпорка` (або поруч із домівкою, якщо ця тека
синхронізується з хмарою: обхід справ став би мережевим і виглядав би як
зависання).

```bash
nysh init D:/Дослідження          # обрати місце при створенні
export NYSHPORKA_WORKSPACE=D:/Дослідження   # закріпити для ВСІХ команд
nysh --workspace D:/Дослідження doctor      # разово, на один запуск
```
```powershell
$env:NYSHPORKA_WORKSPACE = 'D:\Дослідження'   # PowerShell, на поточну сесію
```

Без змінної команди шукають файл `nyshporka.toml` угору від поточної теки — тож
усередині простору нічого вказувати не треба. Куди дивиться застосунок зараз,
каже `nysh doctor`: перевірка «Робочий простір» друкує корінь і джерело
(`env:…`, `marker`, `explicit`).

### Що саме ставимо

Нишпорку ставлять дуже різні люди, і показувати всім однакове — означає комусь
брехати. `nysh init` питає, чим ви користуватиметесь; змінити відповідь можна
будь-коли (`nysh sections`), нічого не перевстановлюючи.

| Набір | Що є | Вага |
|---|---|---|
| `catalog` | каталоги, газетир, описи фондів | без рушіїв |
| `amateur` | + читання рукопису й гортач | + torch |
| `researcher` (типово) | + пошук у прочитаному, облік переглянутого, експорт | + torch |
| `lab` | + місце для розмітки й тренування (поки порожнє) | + torch |

```powershell
powershell -ExecutionPolicy Bypass -File install\windows.ps1 -Preset catalog
```
```sh
NYSH_PRESET=catalog sh install/unix.sh
```

🔴 **Вимкнена частина вимкнена всюди**, а не лише в шапці: її дії відмовляють і
в браузері, і в командному рядку, і в агента — з назвою секції та командою, якою
її увімкнути. Напівстан («кнопки немає, але команда працює») тут гірший за
відсутність: він читається як несправність.

`catalog` — єдиний набір без `torch` (~2.5 ГБ). Він точно описує найпершого
відвідувача: сканів ще немає, відеокарти теж, а питання вже є — «де взагалі
метрики мого села». Обидва вкладені зрізи відповідають на нього одразу після
встановлення, тож це робочий набір, а не урізаний.

Типово ставиться CPU-збірка torch. Задача читання впирається в **ядра
процесора**, не у відеокарту (виміряно: 92% часу CPU при завантаженні карти
близько нуля), тож на CPU все працює — просто повільніше: ~2 хв на сторінку
проти ~20 с. Прискорення відеокартою доставляється окремим кроком
(`nysh doctor` підкаже яким), а не вимагається збіркою.

## Що вже працює

**Два зрізи їдуть разом із пакетом**, тож `nysh find "моє село"` працює
**одразу після встановлення** — ні сканів, ні відеокарти, ні обходу чужих
сайтів:

* **каталог ДАХмО** — 9020 справ 47 фондів, роки 1648-2025;
* **поаркушевий покажчик плівок** — 62 412 записів, 1594 населені пункти:
  яке село на яких КАДРАХ якої плівки. Це найкоротша відповідь на «де метрики
  мого села», і вона не вимагає завантажити жодного байта сканів.

🔴 Зрізи старіють, тож кожна відповідь несе їхню дату й межі покриття
(покажчик плівок сьогодні накриває лише Молдову — в інших регіонах дзеркала
поаркушевого покажчика немає в принципі). Зібране на місці має пріоритет над
вкладеним.

**Каталоги й завантаження.** Переглядачі архівів (ARCHIUM — обласні та
центральні), дзеркало плівок FamilySearch і Wikimedia Commons — за спільним
контрактом джерела: `search` / `browse` / `manifest` / `fetch`. Найцінніше в
дзеркалі — **поаркушевий покажчик плівки**: він відповідає «де метрики мого
села» без жодного завантаження. Найцінніше в Commons — **повний файл**: дзеркала
обрізають великі справи в рази (25 МБ проти 771 МБ), і виглядає обрізане як
нормальна копія.

```bash
nysh find "Ракулешты"                  # де взагалі є щось про село
nysh browse fsfilm moldova             # що лежить у регіоні
nysh get fsfilm "<плівка>" --out . --frames 6-10
nysh crawl archium                     # зібрати каталог справ для пошуку
```

**Що взагалі існує у фонді.** Окреме питання від «що вже оцифровано», і саме
воно вирішує, чи має сенс замовляти документ в архіві. Складають відповідь
збирачі реєстру опису:

```bash
nysh registry sources                                  # які збирачі є
nysh registry plan  duck --repo ДАВіО --fond 904       # скільки це коштуватиме
nysh registry collect archium --repo ЦДІАК --fond 224 --fond-id 198
nysh registry rate                                     # чи витримали темп
nysh registry merge --repo ЦДІАК --fond 224            # звести в один реєстр
```

Приймач збирання — **не число рядків**. Позиційний розбір таблиці опису вже
одного разу віддав 2944 справи з однаковим заголовком, і за кількістю це
виглядало успіхом. Тому кожен запуск друкує, скільки рядків мають роки, аркуші
й заголовок, і **чого джерело не бачить**: позиції «вільний номер» і «Справа
вибула» лягають окремо — пущені в реєстр, вони стають фантомами в черзі, за
якою замовляють документи.

🔴 Duck Inspector — безкоштовний волонтерський сервіс, і його ліміт (5 запитів
на 10 с) міряється **по клієнту**, а не по процесу. Тому запити йдуть через
чергу, спільну на всю машину: дві сесії з бездоганною паузою кожна дали б
подвійний темп. `nysh registry rate` показує максимум у вікні за журналом
фактичних відправок — не за наміром.

**Зведення джерел.** `nysh registry merge` зливає всі `registry/*.tsv` у реєстр
фонду. Джерела майже не перекриваються (ф.230: Вікіджерела 256 справ, ukrfamily
916, спільних **21**), тож жодне окремо не дає й чверті — сенс має саме сума.

Заголовок обирає РАНГ джерела: опис, прочитаний оком зі скану, сильніший за
опис на сайті архіву, той — за чужі транскрипції, а зведені покажчики
найслабші. Слабший заголовок не зникає, а лишається слідом.

🔴 У чергу ока подається не всяка розбіжність. Дві транскрипції однієї таблиці
розходяться словами постійно («Свято-Покровська» проти «Покрови Пресвятої
Богородиці» — та сама церква), і подавати це означає втопити справжнє: на ф.224
таких позицій було 572, на ф.904 — 1606 із 1606. Розбіжністю тут вважається
РІЗНЕ СЕЛО, бо саме воно означає переплутану справу. Вердикт, поставлений
людиною, перезбірку переживає.

⚠ Покриття рахується лише там, де відома межа опису. Немає межі — немає й
відсотка: «0/0 · немає 0» читається як «усе на місці», хоча означає, що
знаменника ніхто не публікував.

**Читання рукопису.** Спершу разово `nysh htr install` і `nysh models get`
(див. вище) — без них команда відмовляє. Далі `nysh read <тека>` або екран
«Читання»: план (скільки
кадрів, яке письмо, яка модель) показується ДО запуску, бо справа читається
годинами. Модель обирається за письмом і за файлом бойових ваг — «найновіша» ≠
«найкраща». Другий рушій читає ті самі кропи: він помиляється ІНАКШЕ й витягує
те, де перший підставив правдоподібне слово.

Рушіїв три, і в кожного своя ділянка. У виводі вони помічені літерою — саме
вона, а не колір, розрізняє їх у чорно-білому терміналі, у логах і при
дальтонізмі:

| | рушій | письмо | де працює |
|---|---|---|---|
| `[П]` | **Писар** | кирилиця | головний голос: канцелярія, метрики, сповідки |
| `[Д]` | **Дяк** | кирилиця | другий голос: тримається пікселів там, де перший додумує |
| `[С]` | **Скриба** | латинка | нотаріат і костельні книги |

Ваги ставляться окремо від пакета — `nysh models get` (Писар v17 · Дяк v4 ·
Скриба v6, разом ~130 МБ, sha256 звіряється завжди). У колесі їх немає
навмисно: пакет із вагами ставився б довго й тому, хто хоче лише подивитись
каталог справ. Власні ваги в `<простір>/data/spotter/models` працюють так само.

**Гортач.** Вирізка рядка з рамкою — щоб було видно, ЗВІДКИ взявся текст.
Виявити ≠ перевірити: машина подає кандидата, вирішує око. Дефолт — рядок, бо
сторінка коштує в десятки разів дорожче (виміряно: 15 КБ проти 1.1 МБ).

**Своя тека стає справою.** Скани, зняті в архіві чи прислані колегою, не мають
шифри — а без ключа в них немає ні обліку прочитаного, ні місця в реєстрі, ні
можливості послатись на знахідку. Екран «Завести справу» (або `nysh case`)
приймає шифру в тих формах, якими її справді пишуть — `ДАХмО 315-1-8433`,
`ф.315 оп.1 спр.8433`, `Ф. 211 Оп. 3 Д. 140`. Опис пишеться **в теку**: вона
переїжджає між дисками й потрапляє до колег, і опис їде з нею. Кнопка ✏ у
переліку відкриває записане для правки — щоб змінити одне слово, а не
передруковувати все наосліп.

**Скани можуть лежати де завгодно.** Не обов'язково всередині простору: тека на
зовнішньому диску чи в мережі береться під облік там, де лежить, — позначкою у
формі, командою `nysh roots add <тека>` або полем `case_roots` у
`nyshporka.toml`. Файли не переносяться. Ціла тека з десятками книг оголошується
БЕЗ шифри (`nysh roots add`); шифра потрібна лише окремій справі
(`nysh case … --adopt`), бо контейнер справою не є.
Розширення зони завжди явне: застосунок ніколи не бере теку сам, бо шлях у
гортач приходить із запиту браузера, і «дозволено все» тут коштувало б надто
дорого. Тека всередині простору лишається записаною відносним шляхом — щоб
простір можна було перенести на інший диск чи віддати колезі.

**Зразкова справа в комплекті.** `nysh sample` (або кнопка на екрані
«Перевірити цю машину») кладе в простір три аркуші справи **ДАХмО ф.315 оп.1
спр.159** — про висвячення в диякони, 1821-1822 — **разом із готовим машинним
декодом двома голосами**. Це відповідь на перше питання після встановлення:
клацнувши рядок у гортачі, видно, ЗВІДКИ взявся текст, а пошук по декоду
знаходить у ньому прізвище. Прочитати ці аркуші заново поки нічим — ваги ще не
викладені, — але весь ланцюг після читання можна пройти до того, як вкладати
власні три тисячі сканів.

**Довідники окремим комплектом.** Газетир зведеного каталогу ЦДІАК (4566
поселень, 348 408 справ) і реєстри опису чотирьох фондів ставляться окремо від
програми — дані оновлюються не тоді, коли код:

```bash
nysh catalog install --from <завантажений zip>   # releases
nysh geog find "Липовеньке"      # де взагалі є документи цього села
```

Газетир відповідає на питання, з якого починається пошук: **які взагалі метрики
цього поселення вціліли і що з них уже у вас**. Шукає обома мовами й латинкою —
`Miastkowka` знаходить те саме, що й кирилицею; раніше такий запит давав нуль, а
це найгірший вид нуля, бо його читають як «такого села немає». І показує три
конфесії окремо: метрики православної громади, костелу й рабинату лежать у
різних фондах, тож шукати лише в православному розділі означає не бачити решти.

**Сховище прочитаного.** Облік того, що вже переглянуто оком — щоб наступна
сесія не гортала ті самі аркуші вдруге. Пошук по прочитаному: у машинному
декоді, у виписаних прізвищах, в учасниках розібраних записів.

**Реєстр справ.** Що є на диску, що прочитано машиною, що прошукано, що бачило
око — з попередженням, коли зріз відстав від джерел.

**Розбір актів у поля.** Читання рукопису дає ТЕКСТ; щоб у таблиці можна було
відфільтрувати «народження 1865 у цьому селі», акт має бути розкладений по
полях — дата події, дата обряду, номер у книзі, учасники з ролями, стан, вік,
місце.

🔴 **Сам розбір робить ВАШ агент і вашим коштом, а не пакет.** Аркуш читає
модель: за нашим виміром це близько 84 тисяч токенів на скан, тобто книга на
дві сотні аркушів — мільйони токенів за прохід, а проходів мусить бути два.
Вбудований виклик чужого API витрачав би ваші гроші з коду, який ви поставили
подивитись каталог. Тому пакет дає те, чого агент сам собі не зробить:

```bash
nysh records prep   "ДАВіО 904-24-24" --scans 0022-0024   # тайли + ЦІНА наперед
#   → агент читає надрукований контракт і тайли, віддає JSON
nysh records ingest "ДАВіО 904-24-24" --file out.json     # валідація + запис
nysh records merge  "ДАВіО 904-24-24" --a гілка1/ --b гілка2/ --apply
nysh records audit  "ДАВіО 904-24-24"                     # чексуми книги
```

* **Тайли обов'язкові.** Розворот метричної книги — це ~4000×3000, модель
  стискає його до ~1568px і бачить у 0.39×: скоропис розсипається. Провал такої
  вичитки виглядає не помилкою, а впевнено неправильним текстом.
* **Повноту доводять чексуми, а не самозвіт.** Метрика нумерує народження й
  смерті двома окремими лічильниками і сама себе рахує наприкінці місяця. Діра
  в нумерації — пропущений акт із точністю до номера. Секція без дір і зі
  збіжним підсумком доведено повна; «агент сказав, що все прочитав» — ні.
* **Один прохід не є джерелом істини.** Модель подає помилкове прочитання так
  само впевнено, як правильне, тож `merge` зводить дві незалежні вичитки: збіг
  іде в сховище, розбіжність — у чергу на людський розсуд.
* **Тип книги — налаштуванням, а не кодом.** Костел, сповідні розписи й
  ревізька казка мають іншу геометрію аркуша, інші лічильники й іншу мову.
  Готові профілі їдуть у пакеті; свої кладуться в
  `<простір>/config/records_profiles.yaml` і накладаються зверху, тож оновлення
  пакета їх не змиває.

Агентові MCP для цього не потрібен: `nysh op records.prep --describe` віддає
повну схему, `nysh op records.prep --args '{…}'` виконує. Перелік MCP-tool'ів
навмисно вужчий — у нього є стеля, за якою модель перестає читати описи.

**Виписка таблицею.** Розібрані акти йдуть у Ексель — щоб фільтрувати роками,
селом, станом і прізвищем, а не гортати.

```bash
nysh export case "ДАВіО 904-24-24" --what acts               # подивитись
nysh export case "ДАВіО 904-24-24" --what all -o книга.xlsx  # забрати файлом
```

Вигляди різні, бо різні питання: `acts` — рядок на акт, ролі розкладені в
колонки (дитина, батько, мати, хрещені); `records` — рядок на учасника, і саме
там фільтруються прізвище, стан і вік; плюс `pages` і `tally`. CSV/TSV
пишуться без жодного додаткового пакета, XLSX потребує `nyshporka[xlsx]`.

🔴 Кожен рядок несе аркуш. Виписка без посилання на скан — переказ: перевірити
її можна лише перечитавши всю справу, тобто ніяк. І підсумки книги («родилось
мужеска 5, женска 4») лежать окремим аркушем, а не серед актів: це чексум
повноти вичитки, а не подія. Свого «разом» таблиця не рахує — у тій самій
книзі трапляються власні `total` і `total_both_sexes`, тож обчислена сума
показала б число, якого в книзі немає.

**Три обличчя, одне ядро.** Браузерна консоль, командний рядок і MCP-сервер для
Claude Code / Codex — тонкі обгортки навколо одного реєстру операцій. Коли
правда одна, вони не можуть розійтись у відповідях; це перевіряється тестом, а
не домовленістю. Працювати з агентом не обов'язково — без нього застосунок
повний.

Агентові при цьому **не потрібен MCP**: `nysh op <ім'я>` дістає будь-яку
операцію реєстру, а `nysh op <ім'я> --describe` віддає схему аргументів і повний
докстрінг, нічого не виконуючи. Перелік tool'ів — зручність для тих середовищ,
де він є, і він за побудовою вужчий: у нього є стеля, за якою модель перестає
читати описи й починає вгадувати.

Якщо агент береться до роботи, він читає [`AGENTS.md`](AGENTS.md) і
[`docs/agents/`](docs/agents/): що вміє, **чого не вміє**, як читати нуль,
де межа, за якою вирішує людина, і як агенти вже помилялися на цьому матеріалі.

## 🔴 Нуль мусить щось означати

Це головне правило проєкту, і воно вбудоване в код, а не в інструкцію.

Порожній результат пошуку — найдорожча відповідь у генеалогії: «немає» закриває
напрям назавжди. Тому джерело, яке **не може** шукати (каталог не зібраний,
дерево регіону не завантажене), не додає нуль до суми — воно відмовляється
відповідати й каже, чого бракує:

```
⚠ archium: каталог справ ще не зібрано, тож шукати нема де — і нуль тут нічого
  не означав би: вбудований пошук сайту індексує лише назви фондів і описів.
⚠ жодне джерело не змогло шукати — цей нуль НІЧОГО не означає
```

Кожна відповідь несе `coverage`: де саме шукали. Кожне попередження їде **полем
конверта**, а не лише в лозі, — інакше саме той читач, який не помітить нічого
поза даними (агент), лишався б без попередження.

## Чого ще немає

Чесно, без замовчувань:

* **Скани ви приносите самі.** Качалки з FamilySearch у пакеті немає й не буде:
  вона вимагає живої сесії в браузері, а масове завантаження суперечить
  правилам сервісу. Дзеркало плівок (`fsfilm`) — це не FamilySearch, і його
  поаркушевий покажчик накриває лише Молдову.
* **Розбір актів у поля робить ваш агент і вашим коштом.** Пакет дає нарізку,
  контракт, валідацію, чексуми й звід — але не читає. Порядок цін названий у
  розділі про `nysh records`, і `records prep` друкує його перед роботою.
  Вбудувати сюди виклик чужого API означало б витрачати ваші гроші з коду,
  який ви поставили подивитись каталог.
* **Класифікатора типу сторінки немає.** Що це за аркуш — метрика, обкладинка,
  вказівник — ставить людина або агент; за пікселями пакет цього не визначає.
* **Цифра з декоду не є фактом.** Вік, суми, номери звіряються із зображенням:
  за прозою стоїть мовна модель, за багатозначним числом — ні.
* **Вицвілий аркуш може не врятувати ніщо.** Обмежує не роздільність, а
  контраст: там, де чорнило й папір розділяє ~37 рівнів яскравості з 255, не
  допомагає ні зум, ні друга вичитка. Таку секцію або перезнімають, або
  приймають неповною — і пишуть це у звіті.

* **Ваги накривають той матеріал, на якому вчились**: кирилиця й латинка
  скоропису XVIII–XIX ст. українських, молдовських і польських архівів. Поза
  ним якість не міряна — і мовчазно поганий текст виглядає так само впевнено,
  як добрий, тож на чужому матеріалі першу справу варто звірити оком.
* **Гортач бачить 86% прогонів.** Переміряно на 614 прогонах: готовий скан у
  519, рендер зі справи-PDF ще у 7. Решта 88 — здебільшого збірки, у яких теки
  однієї справи немає в принципі, і прогони, чия тека лишилась на чужій машині;
  другі лікуються `nysh cases bind`. Для тих, що видно, аркуш тепер показується
  правильно й на рендері теж — раніше кроп рядка там з'їжджав.
* **Покажчик плівок накриває лише Молдову.** Не наша межа: в інших регіонах
  дзеркала `folder_meta` це голий підпис теки, поаркушевого переліку там немає.
* **Описи є не для всіх фондів.** Готові зрізи чотирьох фондів приходять
  довідниками (нижче); решту довелося б збирати самому, а збирачів у цьому
  пакеті немає.
* **Зразок не читається заново — лише все після читання.** Три аркуші справи
  ДАХмО 315-1-159 їдуть у пакеті вже з машинним декодом, тож гортач, пошук і
  реєстр працюють на них одразу; а прогнати по них САМЕ ЧИТАННЯ нічим, доки
  немає ваг. Це та сама межа, що й у першому пункті, і зникне вона разом із ним.
* **Кандидатів нема кому подавати.** Людський gate `nysh review` працює, але
  пишуть у нього fetcher'и чужих сайтів, яких у цій версії немає (питання їхніх
  умов використання в роздаваному продукті). На щойно створеному просторі черга
  порожня — це стан, а не поламка.

## Розробка

```bash
uv sync --group dev
uv run pytest
uv run ruff check . && uv run mypy
pre-commit install            # ворота проти приватних даних
```

### 🔒 Ворота проти приватних даних

Пакет виділяється з приватного дослідницького репозиторію однієї родини, і
головна небезпека тут — не зловмисник, а випадковість: один `git add -A`,
скопійований для прикладу шматок коду з реальним ідентифікатором особи, шлях із
машини автора в докстрінгу.

```bash
python tools/scan_private.py             # робоче дерево
python tools/scan_private.py --staged    # індекс (стоїть у pre-commit)
python tools/scan_private.py --history   # уся історія, перед першим push
```

Перевірка стоїть **до** коміту, бо git не забуває: файл, доданий і видалений
наступним комітом, лишається в історії назавжди, а прибрати його означає
переписати вже опубліковану гілку.

## Політика підписування коду

Підписується **один артефакт** — інсталятор для Windows
`nyshporka-<версія>-setup.exe` зі [сторінки релізів][rel]. Колесо й `sdist` на
PyPI не підписуються Authenticode: там цілість гарантує сам індекс і
[Trusted Publishing][tp] через OIDC, без довготривалих токенів.

**Що підписується й ким.** Файл збирається виключно в GitHub Actions
(`.github/workflows/release.yml`, job `windows-setup`), із тега `v*`, після
воріт якості й перевірки на приватні дані. Ручної збірки на чиїйсь машині в
ланцюгу немає. Проєкт веде одна людина, тож ролі Author, Reviewer і Approver
збігаються; кожен підпис підтверджується окремо, вручну.

**Приймач для вас:**

```powershell
(Get-AuthenticodeSignature .\nyshporka-setup.exe).Status   # Valid
```

Поруч із інсталятором у релізі лежить `.sha256` — ним можна звірити
завантажене незалежно від підпису.

> Free code signing provided by [SignPath.io](https://about.signpath.io),
> certificate by [SignPath Foundation](https://signpath.org)

⚠ **Станом на зараз заявку подано, і випуски ще не підписані.** Доти Windows
показує «Windows захистив ваш ПК» — це очікувано для програми, яку ще мало хто
завантажував. Підпис прибере напис «Невідомий видавець», але саме попередження
зникне лише тоді, коли назбирається репутація: Microsoft скасувала миттєве
довір'я до сертифікатів, зокрема й EV.

[rel]: https://github.com/SERGIUSH-UA/nyshporka/releases
[tp]: https://docs.pypi.org/trusted-publishers/

## Приватність

Нишпорка — локальний застосунок: телеметрії немає, облікових записів немає,
дослідження нікуди не вивантажується, а в мережу застосунок ходить лише тоді,
коли його про це попросили командою. Повний перелік того, куди й навіщо, —
[`PRIVACY.md`](PRIVACY.md).

## Ліцензія

[AGPL-3.0-or-later](LICENSE).

Копілефт тут не вибір настрою, а вимога дерева залежностей: `Unidecode` —
ядрова залежність (нормалізація імен, `utils/text.py`) — під **GPL-2.0-or-later**.
AGPL-3.0 з нею сумісна й суворіша, а застосунок працює через браузер, тобто саме
той випадок, який AGPL і покриває.

⚠ Історична правка: доти тут стояло, що причина — `ultralytics` і `PyMuPDF`.
Обох у залежностях уже немає (PDF читає `pypdfium2` під BSD/Apache), і
обґрунтування пережило свої підстави на кілька релізів. Решта стеку читання
дозвільна: kraken, PARSeq, timm, torch — Apache/BSD.

⚠ Пакет `strhub` містить підмодуль `models/abinet` під non-commercial ліцензією
USTC. Нишпорка використовує з нього **лише PARSeq** (Apache-2.0).

**Ваги моделей — окремо, під [CC BY-SA 4.0](LICENSE-MODELS.md).** У пакеті їх
немає, вони приходять окремим релізом, а AGPL на бінарних вагах не має
визначеного «відповідного вихідного коду» — тобто нічого б не захистила, зате
відлякувала б тих, хто хоче вжити їх чесно. Умови ті самі за духом: вільно,
зокрема комерційно, з атрибуцією, а дотреновані ваги лишаються відкритими.
