⟨/⟩ 50_Domain/Cards/Template/DocumentForm.cs

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

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

namespace Ban.Sdaid.Icd.Domain.Cards.Template
{
    /// <summary>Форма документа — описание бумажного бланка: страницы, разделы, поля.</summary>
    [DomainEntity(Origin = DomainDataOrigin.Reference)]
    public class DocumentForm
    {
        /// <summary>Идентификатор формы.</summary>
        public Guid Id { get; init; }

        /// <summary>Вид документа, который представлен этой формой.</summary>
        [Relation]
        public DocumentKind DocumentKind { get; init; } = null!;

        /// <summary>Наименование бланка, как оно напечатано в заголовке формы.</summary>
        public string Name { get; init; } = string.Empty;

        /// <summary>Доступна ли форма для выбора при вводе.</summary>
        public bool IsActive { get; init; }

        /// <summary>Страницы бланка.</summary>
        [Relation(Kind = RelationKind.Composition, Min = 1)]
        public IReadOnlyList<FormPage> Pages { get; init; } = new FormPage[0];
    }

    /// <summary>Страница бумажного бланка.</summary>
    [DomainEntity(DomainEntityKind.ValueObject, Origin = DomainDataOrigin.Reference)]
    public class FormPage
    {
        /// <summary>Номер страницы, начиная с первой.</summary>
        public int Number { get; init; }

        /// <summary>Заголовок страницы, как он напечатан на бланке.</summary>
        public string Title { get; init; } = string.Empty;

        /// <summary>Роль, заполняющая страницу, если она указана на бланке.</summary>
        public string? FilledBy { get; init; }

        /// <summary>Разделы верхнего уровня на странице.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<FormSection> Sections { get; init; } = new FormSection[0];

        /// <summary>Профиль автоопознания страницы: эталон рамки, линий и зон-различий; null, если не размечен.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public PageDetectionProfile? Detection { get; init; }
    }

    /// <summary>Раздел страницы: озаглавленная группа полей. Разделы вкладываются друг в друга.</summary>
    [DomainEntity(DomainEntityKind.ValueObject, Origin = DomainDataOrigin.Reference)]
    public class FormSection
    {
        /// <summary>Заголовок раздела, как он напечатан на бланке.</summary>
        public string Title { get; init; } = string.Empty;

        /// <summary>Порядок раздела среди соседних.</summary>
        public int Order { get; init; }

        /// <summary>Вложенные разделы.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<FormSection> Sections { get; init; } = new FormSection[0];

        /// <summary>Поля, размещённые непосредственно в этом разделе.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<FormField> Fields { get; init; } = new FormField[0];
    }

    /// <summary>Поле формы: место на бланке, где заполняется атрибут вида документа.</summary>
    [DomainEntity(Origin = DomainDataOrigin.Reference)]
    public class FormField
    {
        /// <summary>Идентификатор поля.</summary>
        public Guid Id { get; init; }

        /// <summary>Атрибут вида документа, который заполняется в этой позиции.</summary>
        [Relation]
        public DocumentKindAttribute KindAttribute { get; init; } = null!;

        /// <summary>Подпись поля, как она напечатана на этом бланке.</summary>
        public string Caption { get; init; } = string.Empty;

        /// <summary>Порядок поля в разделе.</summary>
        public int Order { get; init; }

        /// <summary>Область поля на странице, если она размечена; служит подсказкой сегментации.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public SourceArea? Area { get; init; }
    }

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

        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_NoVersions = """
            ## Почему у формы нет версий

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

            Это следствие раздельного описания состава и формы. Пока состав задавался бланком,
            версии были обязательны — иначе правка задним числом меняла бы смысл готовых документов.
            """;

        public static string S3_Composition = """
            ## Что попадает в форму

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

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

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

            Формы без страниц не бывает: бланк — это лист, и описание бланка без единого листа
            не описывает ничего. Завести форму до наполнения её полями можно, до наполнения
            страницами — нет.
            """;

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

            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_006_CardDataStructure)} — решение о структуре данных информационной карты
            """;
    }

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

        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 = """
            ## Назначение

            Страница — единица, с которой работает пользователь при вводе: каждой странице
            соответствует свой кадр изображения. Заголовок и указание роли берутся с бланка:
            у формы на рукопись страницы прямо подписаны «Хранитель (л. 1)», «Материальная основа.
            Хранитель–Консерватор (л. 3)», «Задание по консервации. Консерватор (л. 5)».

            Число страниц зависит от бланка: у формы на издание их две, у формы на рукопись — пять.
            """;
    }

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

        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 = """
            ## Назначение

            Раздел нужен для показа: он воспроизводит разметку бланка, чтобы пользователь сверял
            документ с изображением, не отыскивая поля глазами.

            В опознании значения разделы не участвуют — значение опознаётся кодом атрибута.
            Поэтому одинаковые по смыслу позиции в разных разделах — «Повреждён» у переплёта
            и у блока — это разные атрибуты, а не один атрибут в двух местах.
            """;
    }

    /// <summary>Описание доменной сущности <see cref="FormField"/>.</summary>
    public class FormFieldSpec : IDomainEntityDocument
    {
        public static string _refName = "поле формы";

        public Type Entity => typeof(FormField);

        public string Name => "Поле формы";

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

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

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

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

            Хранит то, что зависит от бланка: подпись и место. Подпись живёт здесь, а не на атрибуте,
            потому что различается между редакциями: `Кол-во стр.` и `Количество страниц` —
            два поля одного атрибута в разных формах.

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

        public static string S2_Area = """
            ## Область на странице

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

            Область задаётся в процентах от размера страницы, как и у распознанного значения,
            поэтому не зависит от разрешения снимка.
            """;
    }
}