⟨/⟩ 50_Domain/Capture/InputPacket.cs

250 строк · в начало

using System;
using System.Collections.Generic;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd.Domain.Cards.Template;

namespace Ban.Sdaid.Icd.Domain.Capture
{
    /// <summary>Состояние пакета ввода в конвейере обработки.</summary>
    public enum PacketState
    {
        /// <summary>В очереди: ждёт обработки или повторной обработки.</summary>
        Queued,

        /// <summary>Проверка: обработан и взят пользователем в работу.</summary>
        Checking,

        /// <summary>Завершён: по пакету создана информационная карта.</summary>
        Completed,

        /// <summary>Ошибка: вид ИК не опознан и не указан пользователем.</summary>
        Failed,
    }

    /// <summary>Пакет ввода — кадры страниц одной бумажной ИК, поставленные в очередь обработки.</summary>
    [DomainEntity]
    public class InputPacket
    {
        /// <summary>Идентификатор пакета.</summary>
        public Guid Id { get; init; }

        /// <summary>Номер пакета вида П-ГГГГММДД-NNN, присваивается при постановке в очередь.</summary>
        public string Number { get; init; }

        /// <summary>Состояние пакета.</summary>
        [Relation]
        public PacketState State { get; init; }

        /// <summary>Момент постановки в очередь.</summary>
        public DateTime QueuedAt { get; init; }

        /// <summary>Кадры пакета.</summary>
        [Relation(Kind = RelationKind.Composition, Min = 1)]
        public IReadOnlyList<ImageFrame> Frames { get; init; }

        /// <summary>
        /// Форма документа, опознанная при обработке; null — вид ИК не определён.
        /// Задаёт состав страниц, по которым распределяются кадры.
        /// </summary>
        [Relation]
        public DocumentForm? Form { get; init; }

        /// <summary>Уверенность определения вида ИК, 0..1; null — обработка не выполнялась.</summary>
        public double? FormConfidence { get; init; }

        /// <summary>
        /// Виды ИК, отклонённые пользователем, и указанный им верный вид — контекст
        /// повторной обработки.
        /// </summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<FormRejection> Rejections { get; init; }

        /// <summary>Значения полей пакета: распознанные и введённые пользователем.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<PacketFieldValue> Values { get; init; } = new PacketFieldValue[0];

        /// <summary>Запрос к модели распознавания в том виде, в каком он отправлен.</summary>
        public string? RecognitionRequestJson { get; init; }

        /// <summary>Ответ модели распознавания целиком, как он получен.</summary>
        public string? RecognitionResultJson { get; init; }

        /// <summary>Момент последней обработки; null — пакет ещё не обрабатывался.</summary>
        public DateTime? RecognizedAt { get; init; }

        /// <summary>Оператор, сформировавший пакет (факт истории, не меняется).</summary>
        public Guid CreatedByUserId { get; init; }

        /// <summary>Оператор, взявший пакет в проверку; null — пакет свободен.</summary>
        public Guid? AssignedToUserId { get; init; }

        /// <summary>
        /// Пакет помечен удалённым (мягкое удаление): скрыт из очереди, но запись и файлы сохранены — обратимо.
        /// Жёсткое удаление стирает запись и файлы физически и этим флагом не пользуется.
        /// </summary>
        public bool Deleted { get; set; }
    }

    /// <summary>Отклонение опознанного вида ИК: что отвергнуто и что считать верным.</summary>
    [DomainEntity(DomainEntityKind.ValueObject)]
    public class FormRejection
    {
        /// <summary>Отклонённая форма документа.</summary>
        [Relation]
        public DocumentForm? Rejected { get; init; }

        /// <summary>Форма, указанная пользователем как верная; null — пользователь её не знает.</summary>
        [Relation]
        public DocumentForm? Suggested { get; init; }

        /// <summary>Момент отклонения.</summary>
        public DateTime RejectedAt { get; init; }

        /// <summary>Пользователь, отклонивший вид.</summary>
        public Guid RejectedByUserId { get; init; }
    }

    /// <summary>Описание доменной сущности <see cref="InputPacket"/>.</summary>
    public class InputPacketSpec : IDomainEntityDocument
    {
        public Type Entity => typeof(InputPacket);

        /// <summary>Display-текст для типизированных ссылок <c>SdaidAnchors.RefTo&lt;InputPacketSpec&gt;()</c>.</summary>
        public static string _refName = "пакет ввода";

        public string Name => "Пакет ввода";

        public string Description =>
            @"Агрегат модуля ввода: кадры страниц одной бумажной ИК, опознанная форма, значения полей и состояние в конвейере обработки.";

        public string Version => "0.3";
        public string Status => "draft";

        public string[] Comments => new[]
        {
            "2026-08-19. По варианту 5 решения о структуре данных: значения полей пакета вместо распознанных полей и идентификаторов, запрос и ответ модели сохраняются в пакете.",
        };

        public static string S1_Purpose = """
            ## Назначение

            Единица передачи данных из ввода в обработку. Всё, что пользователь снял по одной
            информационной карте, уходит в очередь одним пакетом.

            Пакет собирается **без плана**: пользователь снимает страницы подряд, а какой это вид ИК
            и по какой форме сделан бланк, определяет Сервис при обработке.
            """;

        public static string S2_States = $"""
            ## Состояния

            | Состояние | Когда наступает |
            |---|---|
            | В очереди | пакет поставлен на обработку или возвращён на повторную |
            | Проверка | обработан и взят пользователем в работу |
            | Завершён | по пакету создана информационная карта |
            | Ошибка | вид ИК не опознан и не указан пользователем |

            Состояния заменили прежнюю модель, где статус выводился из последнего подтверждённого
            этапа обработки: этапов было пять, теперь у проверки две вкладки — исходные данные
            и проверка карты, — и выводить статус стало не из чего.

            По этим же состояниям фильтруется очередь.
            """;

        public static string S3_FormRecognition = $"""
            ## Опознание формы

            Форма документа проставляется обработкой вместе с уверенностью её определения. Пока форма
            не опознана, разобрать значения невозможно: состав страниц и полей берётся из неё.

            Если пользователь считает, что вид определён неверно, он отклоняет его и может указать
            верный. Отклонение сохраняется в пакете и уходит в повторную обработку как контекст:
            какая форма отвергнута и какую считать верной. Пакет, у которого форма не опознана
            и не указана, переходит в состояние ошибки.

            Решение и его цена — в {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_007_InputWithoutPlan)}.
            """;

        public static string S35_Recognition = $"""
            ## Обмен с моделью

            Пакет сериализуется в JSON **по структуре формы** и отправляется модели вместе с кадрами;
            ответ сохраняется в пакете целиком и разбирается в значения полей. Интерфейс рендерит
            значения обратно на структуру формы — страницы, разделы, подписи полей.

            Ключом в JSON служит **код атрибута**, а не подпись поля: подпись меняется от бланка
            к бланку, и по ней результат обратно не разложить. Форма задаёт структуру запроса
            и порядок, состав вида — имена.

            Запрос и ответ хранятся полями пакета, а не отдельными сущностями: у них нет
            собственной жизни, они читаются только вместе с пакетом. Повторная обработка переписывает
            результат прежней — история прогонов пока не нужна.

            Устройство самого запроса — {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_009_RecognitionModelIntegration)}.
            """;

        public static string S4_Invariants = """
            ## Инварианты

            - Пакет не создаётся пустым: как минимум один кадр.
            - Число кадров не превышает предела, заданного настройкой; по умолчанию пять.
            - Номер присваивается один раз и не меняется.
            - Автор ввода и исполнитель проверки — разные атрибуты.
            - Правка состава или разметки кадров возвращает пакет в очередь: результаты
              распознавания перестают соответствовать исходным данным.
            - Форма фиксируется у пакета, а не у карты: это обстоятельство ввода, а не свойство
              готового документа.
            - Значения полей принадлежат пакету, а не карте: уверенность, распознанный набор,
              кадр и область — данные о распознавании, а не о результате.
            - Удаление пакета — двух видов: **мягкое** (флаг `Deleted`, пакет скрыт из очереди,
              запись и файлы сохранены — обратимо) и **жёсткое** (окончательное: запись удаляется
              каскадом вместе с кадрами и значениями, файлы кадров стираются из хранилища —
              необратимо). По умолчанию пользователю доступно мягкое; жёсткое — только по праву
              (claim; модель доступа — ADR-011).
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_007_InputWithoutPlan)} — решение о вводе без плана
            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_004_CreatorVsAssignee)} — решение о разделении «кто ввёл» и «кто взял в работу»
            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} — доступ к жёсткому удалению определяется правом (claim)
            """;
    }

    /// <summary>Описание доменной сущности <see cref="FormRejection"/>.</summary>
    public class FormRejectionSpec : IDomainEntityDocument
    {
        public Type Entity => typeof(FormRejection);

        public string Name => "Отклонение вида ИК";

        public string Description =>
            @"Несогласие пользователя с опознанным видом информационной карты: что отвергнуто, что считать верным, кто и когда отклонил.";

        public string Version => "0.1";
        public string Status => "draft";
        public string[] Comments => new string[0];

        public static string S1_Purpose = """
            ## Назначение

            Отклонение — не правка, а задание на повторную обработку. Пользователь не разбирает
            неверно распознанные поля, а одним действием возвращает пакет в очередь и берёт следующий.

            Указанный верный вид — подсказка обработке, а не утверждение: если пользователь его
            не знает, отклонение сохраняется без подсказки, и Сервис пробует определить вид заново,
            зная, какой вариант отвергнут.
            """;

        public static string S2_Invariants = """
            ## Инварианты

            - Отклонения накапливаются: каждая попытка сохраняется, прежние не затираются.
              По ним видно, сколько раз пакет возвращался и что уже отвергнуто.
            - Отклонённая и предложенная формы различаются: предложить отвергнутую нельзя.
            """;
    }
}