# Этапы 0–1 подсистемы «Документооборот» (/agreements/) — отчёт о ходе работ

**Документ** описывает выполненное по ТЗ «Документооборот /agreements/» v5.0 и **как это проверено**. Разработка велась через индекс RAGRAF: результаты измерения пользы индекса — в `docs/03_implementation/POLYGON_REPORT.md`.

**Дата:** 5 октября 2026 г. · **Стенд:** `z_bsdocs_test` (MySQL 9.7.2, PHP 8.5) · **База проверена** как тестовая автоматически (`check_db_target.php`).

---

## 1. Что сделано

### Этап 0 — подготовка среды (выполнен)

| Работа | Результат | Копия для отката |
|---|---|---|
| Включены расширения PHP | `curl`, `fileinfo`, `openssl`, `sodium` (строки 919/923/932/947) | `php.ini.bak-20261005-ragraf` |
| Ключи подсистемы в локальном конфиге | `app.secret_key` (32 байта), `agreements.upload_dir` | `config.local.php.bak-etap0-20261005` |
| Каталог вложений | `agreements/uploads` создан, пуст | — |
| Файл ТЗ приведён к LF | CRLF → LF (проверка конвенций проходила с ошибкой) | `.bak-crlf-20261005` |

### Миграции (10 файлов, применены дважды — критерий идемпотентности 5)

`20261004_0013_create_agr_core` (22 таблицы) · `0014_create_agr_contracts` · `0015_create_agr_payments` · `0016_create_agr_timesheet` · `0017_create_agr_penalties` · `0018_create_agr_vacations` · `0019_create_agr_tasks` · `0020_seed_agr_dictionaries` · `0021_seed_agr_routes` · `0022_seed_agr_employees`

**Итог:** 34 таблицы `Doc*`, 7 ролей, 42 строки `DocRoleJournal`, 3 организации, 43 строки справочников, 30 настроек, 12 шагов маршрутов, 2 сотрудника.

### Ядро подсистемы (`src/Agreements/`)

| Модуль | Назначение |
|---|---|
| `Engine.php` | Маппинг журналов (§4.0): ключ, код, таблица, префикс, страница |
| `Setting.php` | Реестр настроек (§17.1): 30 ключей, шифрование sodium, проверка значений, аудит без значений |
| `History.php` | Единое время подсистемы, история в `AdminAudit`, буфер предупреждений |
| `Access.php` | Права fail-closed (§5), свежее чтение `SuperAdmin`, область из `DocRoleJournal`, роли пользователя, аудит доступа |
| `AccessMatrix.php` | **Матрица «роль × действие» из §5.8** как данные |
| `Workflow.php` | **Маршруты согласования**: шаги, раунды, решения, отзыв, снимок защищённых полей |
| `Actions.php` | **Аннулирование, удаление, восстановление** (вынесено из страницы ради проверяемости) |
| `Numbering.php` | Выдача номеров `SELECT … FOR UPDATE`, формат `PL-2026-0001` |
| `Registry.php` | Единая сборка строк для страницы и поиска, белые списки сортировки |
| `Dictionaries.php` | Справочники для форм: подразделения деревом, организации, контрагенты, договоры, сотрудники |

### Страницы раздела (`agreements/`)

| Файл | Назначение | Состояние |
|---|---|---|
| `index.php` | Список журнала: панель, таблица, пейджер, переключатель журналов | готов |
| `search.php` | JSON-эндпоинт живого поиска (контракт §3.2) | готов |
| `edit.php` | Создание и правка записи, защищённые поля (§5.7), выдача номера | готов |
| `card.php` | Карточка записи: маршрут, реквизиты, история, панель действий | готов |
| `action.php` | Обработчик действий: отправка, согласование, отклонение, отзыв, аннулирование, удаление | готов |
| `parts/toolbar.php` | Поиск, фильтр по статусу, размер страницы, действия | готов |
| `parts/pager.php` | Постраничная навигация, счётчик диапазона | готов |
| `parts/rows.php` | Строки и заголовки таблицы, один код для страницы и поиска | готов |
| `parts/status.php` | Статус со смысловым тоном (только токены дизайн-системы) | готов |
| `parts/emptystate.php` | Четыре разных состояния: пусто, фильтр, нет прав, ошибка | готов |
| `journals/*.php` | Конфигурации **пяти журналов**: DG, PL, SH, OT, TD | готов |

**Двойное назначение `Actions.php` и `Workflow.php`:** правила действий вынесены из страниц и в модули ядра по двум причинам: (1) это предметная область, её будут вызывать задачи по расписанию; (2) страница выполняет `exit` и отправляет заголовки, поэтому её нельзя вызвать в проверке — это выяснилось при написании проверки полного цикла.

**Изменения в существующих файлах:** `src/Bootstrap.php` — подключение модулей подсистемы (копия `Bootstrap.php.bak-agreements-20261005`).

---

## 2. Как проверено

### Автоматические проверки: **336 проверок, 0 провалов**

| Проверка | Проверок | Что покрывает |
|---|---|---|
| `tests/agreements/selftest_agr.php` | 70 | Ядро без БД: маппинг, настройки, валидация, права |
| `tests/agreements/selftest_agr_db.php` | 59 | Ядро с БД, самоочистка, восстановление счётчиков |
| `tests/agreements/check_edit_create.php` | 29 | **Полный цикл создания**: форма → отправка → номер → БД → список → правка → уборка |
| `tests/agreements/check_scope_self.php` | 18 | **Область «свои записи»**: чужая запись не видна ни в списке, ни по прямому номеру |
| `tests/agreements/check_workflow.php` | 57 | **Полный цикл согласования**: маршрут, решения, раунды, защита полей, аннулирование, удаление |
| `tests/agreements/check_journals.php` | 56 | **Связность конфигураций со схемой**: все колонки, поля, статусы и действия существуют |
| `tests/agr_smoke_readonly.php` | 47 | Страницы через HTTP: сессия, права, контракт JSON, негативные случаи |

Прогон выполнен **дважды подряд** — проверена идемпотентность уборки.

### Проверка конвенций проекта

`tools/Invoke-ProjectDoctrine.ps1`: **17 проверок, 0 ошибок, 0 предупреждений, PASS** (включая кодировки, миграции, одну точку подключения к БД, отсутствие склейки SQL, цвета только токенами, синтаксис PHP).

### Что именно проверяется на живом стенде

1. **Без сессии** — страницы и JSON-поиск отдают редирект на вход, сохраняя возврат в раздел.
2. **Под администратором** — список открывается, разметка на месте, ошибок PHP нет.
3. **Под ролью `employee` (область self)** — область self, создание разрешено, «Отметить оплаченным» и «Согласовать» **недоступны**.
4. **Область видимости данными** — чужая запись не появляется в списке и возвращает отказ по прямому номеру.
5. **Негативные случаи** — неизвестный журнал: 404 на странице и 400 в поиске; POST в поиск отклонён; несуществующая запись: 404.
6. **Полный цикл создания** — номер `PL-2026-0001`, сумма `1234,56` разобрана как 1234.56, статус `draft`, запись видна в списке; тестовые данные удалены, счётчик возвращён.

---

## 3. Дефекты, найденные при работе (и исправленные)

| № | Дефект | Как найден |
|---|---|---|
| 1 | Файл ТЗ имел CRLF → проверка конвенций падала на ровном месте | первый прогон доктрины |
| 2 | `Duplicate entry 'DG-3'`: шаг маршрута с альтернативой ролей | применение миграций |
| 3 | **Шифрование настроек молча не работало**: `z_cfg()` делит ключи по точкам, плоский ключ возвращал null | чтение значения после записи |
| 4 | Заголовок миграции обещал 23 таблицы, создаётся 22 | сверка со схемой |
| 5 | `History.php` писал в колонку `User`, в таблице — `UserID` | ошибка драйвера при первой записи истории |
| 6 | **Права расширены**: роль `employee` получала `approve`, `mark_paid`, `sign`, `archive` | проверка под ролью сотрудника на стенде |
| 7 | **`AuthorUserID`/`UpdatedBy` не существуют**: в журналах `InitiatorID` | ошибка 42S22 при первом сохранении |
| 8 | **Сигнатуры аудита не совпадали**: `z_agr_acl_audit()` принимает массив | `TypeError` в логе |
| 9 | **Обязательное поле с пустым справочником** делало форму неотправляемой без объяснения | проверка цикла создания |
| 10 | **Устаревший opcache** в долгоживущем `php -S` давал ложную ошибку в `Shell.php` | сравнение стендов с opcache и без |
| 11 | **Отклонённая запись — тупик**: нет ни одного доступного действия | проверка полного цикла согласования |
| 12 | **Шаг с альтернативными ролями нельзя согласовать** (`UserID = NULL`) | проверка полного цикла |
| 13 | **Защита полей §5.7 не работала**: `ProtectedJson` не заполнялся | проверка полного цикла |
| 14 | `SubmittedAt`/`SubmittedBy` не существуют в журналах | **предотвращён**: `op_schema` индекса показал отсутствие колонок до написания кода |

**Дефект 6 — самый серьёзный по последствиям:** любой сотрудник мог бы отметить платёж оплаченным и подписать документ. Исправлен введением матрицы §5.8 как **данных** (`AccessMatrix.php`): раньше группа действий возвращала `true` «любому, кому выдан журнал».

**Дефекты 11–13 найдены проверкой полного цикла согласования** (57 проверок). Все три — класса «работает, но не так»: страница открывалась, ошибок не было, но маршрут обрывался, а защита не действовала.

**Дефект 10 — не дефект кода:** ошибка возникала только на стенде, поднятом до правок. Вывод для методики: при проверке на локальном `php -S` стенд надо перезапускать или отключать opcache.

Подробный разбор эффекта от индекса RAGRAF — в `docs/03_implementation/RAGRAF_EFFECT_REPORT.md`. Решения по двум спорным вопросам ТЗ — в `docs/04_research/WORKFLOW_ANALOGS.md`.

---

## 4. Что осталось по этапу 1

| Страница | Назначение | Приоритет |
|---|---|---|
| `card.php` | Карточка записи: поля, история, вложения, действия | высокий |
| `action.php` | Обработчик действий: отправка, согласование, отказ, отмена | высокий |
| `rows.php` | Отдача строк для таблицы (отдельный эндпоинт, если понадобится) | средний |
| `journals/payments.php` | Журнал платежей описан (`pl.php`); нужны конфигурации DG/OT/SH/VK/TD | средний |
| `timesheet.php` | Табель — отдельная страница (§4.0) | средний |
| `templates/*.htm` | Перенос разметки в шаблоны проекта (§8.2) | средний |
| `src/modules/agreements.php` | Модуль главной страницы для раздела | низкий |
| `rights.php`, `export.php`, `file.php`, `inline.php` | Права, экспорт, вложения, быстрая правка | по §15 |

**Не начатые модули ядра:** `Delegation.php` (частично покрыт `Access.php`), `Workflow.php` (маршруты), `Sign.php` (подпись), `Periods.php` (табель), `Retention.php` (сроки хранения), `Attachments.php`, `Export.php`, `Departments.php`.

---

## 5. Требуется решение владельца

1. **Формат альтернативы ролей в маршруте.** §6.1 говорит «`director` **или** `opr_director`», но §3.1/§6.2 требуют одну строку с одним `RoleKey`. Принято решение: одна строка `RoleKey='director,opr_director'`, `AnyOf=1`. Если владелец считает иначе — правило меняется в одном месте (`accessmatrix` + миграция 0021).
2. **Сверка боевой `bsdocs` с инвентарным списком** (§13.2 п.5) не выполнялась: боевая база вне разрешённой области работ.
3. **Согласование ролей в журнале PL.** По маршруту согласуют `head` → `accountant` → `director/opr_director`. Матрица §5.8 допускает для `employee` согласование только при условии. Сейчас `employee` в журнале PL **не может** согласовывать — это соответствует маршруту и §6.2 проекта. Требуется подтверждение, что §6.2 приоритетнее строки матрицы с условием (2).
