#import "common.typ": pz-fig, pz-table

= Программная реализация

== Разработка программных модулей

Архитектура реализации следует принятой на этапе проектирования схеме MVVM + Clean Architecture. Модули Gradle разделены по ответственности: контракты vault, доменная логика, сценарии use case, Android-инфраструктура (Room, OAuth, файловые адаптеры), UI и точка входа `:app`. Такое разбиение позволило параллельно развивать локальный и удалённый контуры и изолировать криптографию от представления данных.

=== Модуль криптографической защиты данных

Класс `Encryptor` формирует `StorageEncryptionInfo`, проверяет ключ и выполняет шифрование/дешифрование на клиенте. Unit-тесты подтверждают корректность для верного и неверного ключа (гл. 5).

=== Модуль управления storage и шифрованием

Use case `ManageStoragesEncryptionUseCase` инкапсулирует проверку `canEncrypt`, включение шифрования и открытие хранилища. ViewModel предотвращает повторный запуск шифрования для занятого storage.

Фрагмент логики включения шифрования:

```kotlin
fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) {
    val key = EncryptKey(password)
    viewModelScope.launch {
        when (manageStoragesEncryptionUseCase.canEncrypt(storage)) {
            ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
                manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
                manageStoragesEncryptionUseCase.openStorage(storage, key, true)
            }
            ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> { /* сообщение */ }
            else -> { /* неподдерживаемая операция */ }
        }
    }
}
```

Блок-схема сценария – рис. @fig-21.

#pz-fig("fig_21_encrypt_flow.png", [Блок-схема: enableEncryption → checkKey → openStorage], "fig-21")

=== Модуль адаптеров хранилищ

`VaultsManager` агрегирует один `LocalVault` и удалённые vault; адаптеры реализуют доступ к файлам внутри каждого `IStorage`. Регистрация удалённых vault – через модуль `:vault-contracts`.

=== Модуль синхронизации хранилищ

В Room хранится сущность `DbStorageSyncGroup` – набор UUID `Storage`, которые должны иметь согласованное состояние. Запуск синхронизации выполняется через `RunStorageSyncUseCase` / `WorkManager` и debounce при изменении файлов (рис. @fig-01–@fig-03). Движок `StorageSyncEngine` (модуль `:usecases`) реализует согласование журналов изменений; доработка касается в основном политики фонового расписания и UX отображения прогресса.

Для каждого `Storage` ведётся журнал изменений по относительным путям пользовательских файлов. Запись журнала (`StorageSyncJournalEntry`) содержит операцию (`UPSERT`, `TRASH`, `DELETE`) и ревизию (`sequence`, `actorId`, `createdAt`). Ключи шифрования в обмен не включаются – провайдер видит только зашифрованные объекты. Модуль `:task-runtime` обслуживает длительные задачи шифрования и синхронизации без блокировки UI.

=== Алгоритм согласования журналов синхронизации

Синхронизация одной группы выполняется в несколько этапов (рис. @fig-35).

#pz-fig("fig_35_sync_merge_algorithm.png", [Алгоритм согласования журналов (StorageSyncEngine)], "fig-35")

*Подготовка.* По UUID из `DbStorageSyncGroup` загружаются объекты `IStorage`. Если в группе меньше двух хранилищ или несовместимы параметры шифрования, синхронизация пропускается. На каждом accessor запрашивается блокировка sync (lease на удалённом диске – best-effort; внутри процесса группа сериализуется `Mutex`). Параллельно для каждого storage вызываются `flushPendingSyncJournal()` и `readSyncJournal()`; служебные пути отфильтровываются (`StorageSyncPaths.isSyncableUserPath`).

*Слияние журналов.* Объект `StorageSyncJournalMerge` объединяет журналы в отображение «путь → победитель». Для каждого пути остаётся запись с наибольшей ревизией. Сравнение реализовано функцией `compareEntries`: сначала `revision.sequence`, при равенстве – `actorId`, затем `createdAt`. Такой порядок обеспечивает детерминированный выбор одной записи при конкурентных изменениях на разных устройствах.

*Выбор источника для пути.* После слияния для каждой пары (путь, `winnerEntry`) вызывается `findSourceStorage`:

- при `UPSERT` – первый `Storage`, у которого запись по этому пути *совпадает* с победителем (`compareEntries == 0`): оттуда читаются байты для копирования;
- при `DELETE` или `TRASH` – первый `Storage`, где путь ещё присутствует в журнале (или любой storage группы, если записей нет): оттуда инициируется удаление на целях.

Если для `UPSERT` источник не найден, путь пропускается (файл уже отсутствует на всех носителях).

*Распространение на цели.* Для каждого `target` в группе, отличного от источника, сравнивается ревизия записи на цели с `winnerEntry`. Если цель уже не слабее победителя (`compareEntries(target, winner) >= 0`), шаг пропускается. Иначе вызывается `applyEntry`: для `UPSERT` – потоковое копирование `openRead` → `openWrite`; для `DELETE` / `TRASH` – `delete` или `moveToTrash`. Все операции выполняются с `recordSyncJournal = false`, чтобы не порождать цикл повторной синхронизации. Ошибки отдельных путей учитываются счётчиком `applyFailures`, отмена кооперативная (проверка `syncGeneration`, снятие блокировок в `finally`).

Фрагмент сравнения ревизий и выбора источника:

```kotlin
private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int {
    val seqCmp = a.revision.sequence.compareTo(b.revision.sequence)
    if (seqCmp != 0) return seqCmp
    val actorCmp = a.revision.actorId.compareTo(b.revision.actorId)
    if (actorCmp != 0) return actorCmp
    return a.revision.createdAt.compareTo(b.revision.createdAt)
}

private fun findSourceStorage(..., winnerEntry: StorageSyncJournalEntry): IStorage? {
    if (winnerEntry.operation == DELETE || winnerEntry.operation == TRASH) {
        return storages.firstOrNull { entriesByStorage[it.uuid]?.get(path) != null }
            ?: storages.firstOrNull()
    }
    return storages.firstOrNull { storage ->
        val entry = entriesByStorage[storage.uuid]?.get(path) ?: return@firstOrNull false
        compareEntries(entry, winnerEntry) == 0
    }
}
```

*Ограничения модели.* Механизм не является полноценным CRDT: конфликты снимаются фиксированным порядком ревизий, а не автоматическим слиянием содержимого. Содержимое файла при расхождении версий без роста `sequence` на одном пути не анализируется побайтно. Шифротекст передаётся как есть; расшифровка на стороне провайдера не предполагается. Корректность алгоритма проверена unit-тестами `StorageSyncEngineTest` (гл. 5): слияние одной записи на путь, пропуск цели с актуальной ревизией, копирование и удаление, cooperative cancellation.

=== Использование средств ИИ при разработке

Разработка Wallenc велась в два этапа. На первом этапе исполнитель самостоятельно спроектировал доменную модель (иерархия vault → storage → файлы, единый `VaultsManager`), навигацию между экранами, визуальный стиль UI на Jetpack Compose @smith-jetpack-compose, границы Gradle-модулей и принципы сопровождаемого кода @martin-clean-code и каркас use case-слоя. Криптографический контур (`Encryptor`, привязка ключей к storage), журнал синхронизации и сценарии OAuth проектировались и проверялись вручную. При проектировании модуля `:domain` была составлена диаграмма классов (рис. @fig-24), фиксирующая первоначальные интерфейсы и связи сущностей до этапа разработки с ИИ-ассистентом.

#pz-fig("fig_24_domain_class_manual.png", [Диаграмма классов модуля domain (самостоятельное проектирование)], "fig-24")

На втором этапе, после готовности архитектурного каркаса, наращивание функционала выполнялось с помощью среды Cursor (модели семейства Composer): адаптеры Yandex Disk и локального storage, движок `StorageSyncEngine`, экраны 2FA и текстовых секретов, unit-тесты. После каждой генерации код просматривался в diff, запускались модульные тесты (`./gradlew :usecases:test` и смежные модули), критичные сценарии проверялись на устройстве.

#pz-table(
  [Роли при разработке (фрагмент)],
  3,
  table.header([Этап], [Исполнитель], [Инструмент / метод]),
  [Домен, навигация, UI-концепция], [Исполнитель ВКР], [Ручное проектирование, Compose],
  [Адаптеры, sync, тесты, доработка UI], [Исполнитель ВКР + ревью], [Cursor, Gradle test],
  [Шифрование, OAuth, ревизии журнала], [Исполнитель ВКР], [Ручная ревизия, без автогенерации «вслепую»],
) <tbl-ai-dev>

ИИ использовался как ускоритель шаблонного и повторяющегося кода, а не как замена проектных решений. Риски (неверные сигнатуры API, лишние зависимости, утечки в логи) снижались обязательной проверкой сборки, отсутствием секретов в репозитории и правилами `.gitignore` для локальных конфигураций.

== Разработка мобильного приложения на Kotlin (Android)

=== Слой domain

Модуль `:domain` содержит интерфейсы хранилищ и use case. Модуль `:usecases` связывает сценарии приложения.

=== Слой data

Модуль `:infrastructure-android` реализует Room `AppDb` (версия 5) с сущностями `DbStorageKeyMap`, `DbStorageMetaInfo`, `DbYandexAccount`, `DbStorageSyncGroup`:

```kotlin
@Database(
    entities = [
        DbStorageKeyMap::class,
        DbStorageMetaInfo::class,
        DbYandexAccount::class,
        DbStorageSyncGroup::class,
    ],
    version = 5,
    exportSchema = false,
)
abstract class AppDb : IAppDb, RoomDatabase()
```

=== Слой presentation

Модуль `:ui` и `:app` содержат Compose-экраны, ViewModel и навигацию. OAuth Яндекс запускается из UI удалённых vault:

```kotlin
remoteVaultAuthenticator.beginLink(CloudBrand.YANDEX) { outcome ->
    when (outcome) {
        is VaultLinkOutcome.Success ->
            viewModel.onVaultLinked(outcome.registration)
        is VaultLinkOutcome.Failed -> { /* ошибка */ }
        VaultLinkOutcome.Cancelled -> { }
    }
}
```

== Взаимодействие подсистем и итоговая архитектура

Зависимости модулей Gradle показаны на рисунке @fig-23. Полный исходный код модулей сборки приведён в приложении А.

#pz-fig("fig_23_module_deps.png", [Зависимости модулей Gradle], "fig-23")

В основном тексте приведены показательные фрагменты; полные листинги – в приложении А.

#include "ch04-expand.typ"
#include "ch04-modules.typ"
