⟨/⟩ Ntr/35_ChangeRequests/DOC_CR_2026_08_31_NegativeTemperature.cs

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

using Rugrid.Sdaid.Notation.Documents;
using Rugrid.Sdaid.Epas.Ntr.Backend.Calc;

namespace Rugrid.Sdaid.Epas.Ntr.ChangeRequests
{
    /// <summary>
    /// Замечание: среднюю температуру наружного воздуха нельзя ввести отрицательной.
    /// Ограничение — в общем компоненте ввода фронтенда, не в правилах предметной области.
    /// </summary>
    public class DOC_CR_2026_08_31_NegativeTemperature : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtrChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<DOC_TtrCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Domain.ConsumptionModel.DOC_HeatingPeriodModel>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Ntr.Ui.Pages.DOC_ModelCard>(),
        };

        public static string _refName = "CR 2026-08-31: отрицательная температура";

        public string Name => "Нельзя ввести отрицательную среднюю температуру наружного воздуха";

        public string Description =>
            @"В карточке модели поле «Отопительный период. Средняя температура наружного воздуха» не принимает значения меньше нуля. Ограничение задано общим компонентом ввода фронтенда; методика и валидаторы расчёта отрицательные температуры допускают.";

        public string Version => "1.0";
        public string Status => "done";
        public string[] Comments => new string[0];

        public static string Source = """"
            ## Источник (как есть)

            Отопительный период. Средняя температура наружного воздуха - нельзя внести значение
            меньше нуля.
            """";

        public static string Analysis = $$""""
            ## Разбор

            **Замечание подтверждается, ограничение лежит на фронтенде.** Поле объявлено с режимом
            ввода `decimal` (`heating-period.util.ts`), собственных правил валидации у него нет —
            ограничение целиком наследуется от общего компонента ввода `ntr-input`
            (`Ntr.Web/src/app/ui/input/input.component.ts`):

            - обработчик нажатия клавиши в режиме `decimal` пропускает только цифру и запятую,
              всё остальное отбрасывает; минуса в списке разрешённых символов нет;
            - вставка из буфера чистится тем же правилом — `sanitizeDecimal` вырезает всё, кроме
              цифр и запятой, поэтому «−12,5» превращается в «12,5».

            **Предметная область отрицательные значения допускает.** Методика задаёт правило
            $t_{ext} \in [-50; 50]$ °C — см. {{SdaidAnchors.RefTo<TeSpecInternalHeatGainChar>("te.spec_internal_heat_gain_char")}};
            валидатор секции проверяет именно этот диапазон, домен хранит `decimal?` без ограничения
            знака. То есть данные корректны и расчёт с ними работает — их нельзя только ввести.

            **Ограничение общее, а не только для температуры.** Режим `decimal` используют все
            вещественные поля карточки модели: габариты, объёмы, уровни, мощности, время работы.
            Для большинства из них запрет отрицательных значений уместен, поэтому снимать его в
            самом компоненте нельзя — нужен явный признак у поля.

            ### Открытые вопросы

            - **OQ-1. Какие поля должны принимать отрицательные значения.** Кандидаты — обе
              температуры: «Средняя температура наружного воздуха» (в обеих группах: справочные
              данные и данные пользователя) и «Средняя температура воздуха внутри здания за ОП»
              (методика допускает для неё тот же диапазон). Остальные вещественные поля — габариты,
              объёмы, мощности — остаются неотрицательными.
            - **OQ-2. Нужна ли проверка диапазона на форме.** Сейчас выход за $[-50; 50]$ ловится
              только расчётом, в виде ошибки после запуска. Показывать ли ошибку сразу при вводе —
              отдельное решение, за пределами этого замечания.
            """";

        public static string BlastRadius = $$""""
            ## Blast radius

            **Только фронтенд.** Домен, БД, контракты API и расчёт правок не требуют: отрицательные
            значения ими уже поддержаны.

            | Слой | Правка |
            |---|---|
            | `ui/input/input.component.ts` | новый режим ввода (или признак «допускать отрицательные») — разрешить ведущий минус в обработчике нажатия и сохранить его при чистке вставленного значения |
            | `model/utils/heating-period.util.ts` | признак у полей средней температуры наружного воздуха — справочные данные и данные пользователя |
            | `model/utils/operating-conditions.util.ts` | то же для средней температуры воздуха внутри здания — по решению OQ-1 |
            | {{SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Ntr.Ui.Pages.DOC_ModelCard>()}} | описание поведения поля, если правка меняет описанный контракт ввода |

            Проверить заодно, что минус допускается только первым символом и в единственном
            экземпляре — иначе чистка вставленного значения пропустит «1-2».
            """";

        public static string Plan = """"
            ## План

            1. Закрыть OQ-1 — перечень полей, принимающих отрицательные значения.
            2. Признак «допускать отрицательные» в компоненте ввода: обработка нажатия клавиши,
               чистка вставки, нормализация значения.
            3. Проставить признак у полей из OQ-1.
            4. Проверить ввод, вставку из буфера и сохранение отрицательного значения.
            5. Зафиксировать «Итог».
            """";

        public static string Acceptance = """"
            ## Acceptance criteria

            - **AC-1.** В карточке модели в поле «Средняя температура наружного воздуха» вводится
              отрицательное значение; после сохранения и перезагрузки страницы оно отображается
              прежним.
            - **AC-2.** Вставка отрицательного значения из буфера обмена сохраняет знак.
            - **AC-3.** Модель с отрицательной температурой наружного воздуха рассчитывается,
              значение градусо-суток отопительного периода растёт по сравнению с нулевой температурой.
            - **AC-4.** Поля, для которых отрицательные значения не предусмотрены (габариты, объёмы,
              мощности, время работы), минус по-прежнему не принимают.
            """";

        public static string Result = """"
            ## Итог

            **OQ-1 закрыт.** Отрицательные значения разрешены обеим температурам:
            «Средняя температура наружного воздуха (данные пользователя)» и «Средняя температура
            воздуха внутри здания за ОП». Поле справочных данных — только для отображения (не
            редактируемый ввод), правки не требовало. Остальные вещественные поля остаются
            неотрицательными.

            **Реализация (фронтенд) — `выполнено`:**
            - `ui/input/input.component.ts` — новый признак поля `allowNegative` (по умолчанию
              `false`). В режиме `decimal` ведущий минус разрешён только первым символом и в
              единственном экземпляре; нормализация значения (в т.ч. вставка из буфера) сохраняет
              ведущий минус.
            - `ui/attributes/attributes.component.ts` / `.html` — признак `allowNegative` проброшен
              из конфигурации контрола в `ntr-input`.
            - `model/utils/heating-period.util.ts` — `allowNegative: true` у «Средней температуры
              наружного воздуха (данные пользователя)».
            - `model/utils/operating-conditions.util.ts` — `allowNegative: true` у «Средней
              температуры воздуха внутри здания за ОП».

            Домен, БД, контракты API и расчёт не менялись — отрицательные значения ими уже
            поддержаны (`decimal?`, диапазон [-50; 50] °C в валидаторе).

            **Статус acceptance criteria:**
            - AC-1 (ввод отрицательного значения) — `выполнено`: проверено в браузере на реальном
              компоненте `ntr-input` — ввод «-12,5» принимается.
            - AC-2 (вставка из буфера сохраняет знак) — `выполнено`: вставка «-8,3» сохраняет знак,
              в FormControl попадает «-8,3».
            - AC-3 (расчёт с отрицательной наружной температурой, рост градусо-суток) —
              `выполнено`: проверено пользователем на живом стенде. Соответствует логике расчёта:
              формула $D_d = (t_{int} - t_{ext}) \cdot z_{ht}$ при отрицательной $t_{ext}$
              увеличивает ГСОП; валидатор секции `te.heating_degree_days` допускает
              $t_{ext} \in [-50; 50]$ °C.
            - AC-4 (прочие поля минус не принимают) — `выполнено`: признак по умолчанию `false`;
              проверено в браузере — габаритное поле отбрасывает минус («-7,1» → «7,1»).

            **OQ-2 (валидация диапазона на форме)** — `отложено`: за пределами этого замечания.
            """";
    }
}