⟨/⟩ Nts/35_ChangeRequests/DOC_CR_2026_11_03_ReferenceConsumption.cs

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

using Rugrid.Sdaid.Notation.Documents;

namespace Rugrid.Sdaid.Epas.Nts.ChangeRequests
{
    /// <summary>
    /// Эталонная норма и эталонный расход топлива: новая расчётная пара величин рядом с
    /// существующими базовой нормой и нормативным расходом — минимальная норма по всей базовой
    /// группе (вид ТС + вид топлива), независимо от отбора пользователя, и расход, посчитанный
    /// той же методикой с подстановкой этой нормы вместо базовой.
    /// </summary>
    public class DOC_CR_2026_11_03_ReferenceConsumption : IChangeRequestDocument, IHasStructuralLinks, IFolder<NtsChangeRequestsFolder>
    {
        public SdaidLink[] Links => new[]
        {
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_ModificationSelectionModal>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleCard>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Ui.DOC_VehicleRegistry>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationGroupSelection>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_NtsCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_InputModel>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_OutputModel>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleModification>(),
            Rel.Uses<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.EPIC_NtsFuelCalc>(),
        };

        public static string _refName = "CR 2026-11-03: эталонная норма и эталонный расход";

        public string Name => "Эталонная норма и эталонный расход топлива";

        public string Description =>
            @"Эталонная норма — минимальная базовая норма по всей базовой группе ТС (вид ТС + вид топлива), независимо от отбора пользователя; эталонный расход — результат той же методики расчёта с подстановкой эталонной нормы вместо базовой. Выводится в окне подбора модификаций, карточке ТС и реестре ТС рядом с существующими базовой нормой и нормативным расходом.";

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

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

            Эталонная норма: расчёт эталонной нормы как минимальной базовой нормы по группе ТС Вида
            ТС и Вида Топлива; Вывести её отдельно в окне подбора модификаций, сохранить, показать в
            карточке ТС рядом с базовой нормой.
            Расчёт эталонного расхода: тот же алгоритм, что и для нормативного расхода, с той
            разницей, что вместо базовой нормы передаётся эталонная норма. Результат показать в
            карточке ТС, сохранить в VehicleCalc.
            Показать эталонный расход в реестре ТС рядом с нормативным.
            """;

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

            1. **Эталонная норма $H_эт$ — свойство базовой группы, не отбора.** Базовая группа
               строится правилом 2 {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.ModificationSelection.DOC_ModificationGroupSelection>()}
               (раздел «Базовая группа»): все модификации вида ТС с установленной нормой, мягко
               суженные по виду топлива. $H_эт$ — минимум этой же «установленной нормы» (поле
               решает {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Epics.NtsFuelCalc.OQ_BaseNorm>()}, то
               же, что и для $H_s$) по **всей** базовой группе, а не по «Отобранным». Отсюда следствие:
               в отличие от $H_min$/$H_max$/$H_s$, которые пересчитываются при любом изменении
               «Отобранных» ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_ModificationSelectionModal>()},
               правило 5), $H_эт$ не меняется от действий пользователя со списками в открытом
               окне — только от смены вида ТС или вида топлива самого ТС (а значит — от пересборки
               базовой группы при открытии окна или при обновлении из АИС).
            2. **Храним по аналогии с $H_s$.** $H_эт$ не хранится как отдельная НСИ-величина
               справочника и не кэшируется отдельно от расчёта: она вычисляется при открытии окна
               подбора (показ) и сохраняется в
               {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Domain.DOC_VehicleCalc>()} по кнопке
               «Применить» — в тот же момент и тем же действием, что и $H_min$/$H_max$/$H_s$
               ({SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Ui.DOC_ModificationSelectionModal>()},
               правило 8). Это решение этой сессии: явно не заводим отдельного постоянного
               хранилища «эталонной нормы вида ТС и топлива» вне расчёта конкретного ТС — его
               нет и у базовой нормы.
            3. **Эталонный расход — тот же алгоритм, параметризованный источником нормы, не
               дубликат формул.** {SdaidAnchors.RefTo<Rugrid.Sdaid.Epas.Nts.Backend.Calc.DOC_NtsCalc>()}
               запускается второй раз с тем же входом, кроме одной замены: везде, где ветка
               использует базовую норму $H_s$ (`fuel.q_mileage` и производные от неё в ветках
               `fuel.passenger_car`/`fuel.bus`/`fuel.truck`/`fuel.dump_truck`/`fuel.special_vehicle`),
               подставляется $H_эт$. Описываем это не вторым набором секций-формул (нарушило бы
               правило одного факта), а новой секцией `fuel.reference_consumption`, которая ссылается
               на существующие секции веток и явно называет единственное отличие — вход `fuel.base_norm`
               заменён на `fuel.reference_norm`.
            4. **Объём результата — по требованию, не полный набор промежуточных итогов.** В
               карточке и реестре показывается только итоговый эталонный расход $Q_эт$ (аналог
               $Q_н$) — как и просил пользователь («результат показать», «показать рядом с
               нормативным», в единственном числе). Промежуточные эталонные $Q_эт^зима$,
               $Q_эт^гор$ и т. п. не вводятся отдельными полями — решение явно зафиксировано,
               можно пересмотреть отдельным CR, если понадобится детализация.
            """;

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

            ### Спецификация

            | Документ | Правка |
            |---|---|
            | `Nts/55_Backend/ModificationSelection/ModificationGroupSelection.cs` | раздел «Базовая группа» — пункт про $H_эт$: минимум нормы по всей базовой группе |
            | `Nts/55_Backend/Calc/NtsCalc.cs` | новая секция `fuel.reference_norm` (аналог `fuel.base_norm`, минимум по базовой группе) и `fuel.reference_consumption` (та же ветка, вход заменён) |
            | `Nts/55_Backend/Calc/InputModel.cs` | новое поле `ReferenceGroupConsumptionRates` — нормы всей базовой группы (не только отобранных) |
            | `Nts/55_Backend/Calc/OutputModel.cs` | новые поля `ReferenceConsumptionRate` ($H_эт$) и `ReferenceNormConsumption` ($Q_эт$) |
            | `Nts/50_Domain/VehicleCalc.cs` | новые атрибуты `ReferenceConsumptionRate`, `ReferenceNormConsumption` |
            | `Nts/65_Ui/ModificationSelectionModal.cs` | итоговая строка — добавлен $H_эт$; правило 5 (расчёт), правило 8 («Применить» сохраняет и его) |
            | `Nts/65_Ui/VehicleCard.cs` | поле «Эталонная норма расхода топлива, $H_эт$» рядом с базовой нормой (группа «Подбор модификации ТС»); поле «Эталонный расход, $Q_эт$, л» рядом с $Q_н$ (группа «Результаты расчёта») |
            | `Nts/65_Ui/VehicleRegistry.cs` | колонка «Эталонный расход, л» рядом с «Нормативный расход, л» |
            | `Nts/60_Epics/NtsFuelCalc/EPIC_NtsFuelCalc.cs` | OQ-25 (хранить ли $H_эт$ вида ТС/топлива отдельно от расчёта ТС), OQ-26 (нужна ли детализация эталонного расхода по периодам) |

            ### Прототип

            | Файл | Правка |
            |---|---|
            | `Ntr/65_Ui/html/shell.js`, `baseGroup`/`openNormTool`/`render` | вычисление $H_эт$ = минимум нормы по `baseGroup(kind, fuel)` (не по «Отобранным»); вывод в итоговой строке окна |
            | `Ntr/65_Ui/html/shell.js`, `computeNorm`/`finalFromBase` | расчёт $Q_эт$ той же веткой с $H_эт$ вместо $H_s$; запись в `RESULT_FIELDS`, `putCalc`, `NTS_STATE` (`qReference`) |
            | `Ntr/65_Ui/html/shell.js`, `NTS_REGISTRY_COLUMNS` | новая колонка «Эталонный расход, л» из `NTS_STATE[id].qReference` |
            | `Nts/65_Ui/html/vehicle-card.html` | строка «Эталонная норма расхода топлива, $H_эт$» и строка «Эталонный расход, $Q_эт$, л» |
            | `Nts/65_Ui/html/vehicle-registry.html` | заголовок колонки «Эталонный расход, л» |
            """;

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

            1. Спецификация: `ModificationGroupSelection.cs`, `NtsCalc.cs`, `InputModel.cs`,
               `OutputModel.cs`, `VehicleCalc.cs`, `ModificationSelectionModal.cs`, `VehicleCard.cs`,
               `VehicleRegistry.cs`.
            2. Прототип: `shell.js` (вычисление, расчёт, реестр), `vehicle-card.html`,
               `vehicle-registry.html`.
            3. Собрать (`dotnet build`, `build-html.cmd`).
            4. `doc`/`testovich` проверяют соответствие (отдельный проход, не этой задачей).
            5. Зафиксировать «Итог».
            """;

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

            - **AC-1.** Окно подбора модификаций показывает эталонную норму $H_эт$ в итоговой
              строке, рядом с $H_min$/$H_max$/$H_s$; значение не меняется при изменении
              «Отфильтрованных»/«Отобранных», только при смене вида ТС или вида топлива ТС.
            - **AC-2.** «Применить» сохраняет $H_эт$ в расчёте ТС вместе с $H_s$.
            - **AC-3.** Карточка ТС показывает эталонную норму рядом с базовой нормой и эталонный
              расход рядом с нормативным; при пустой базовой группе — прочерк.
            - **AC-4.** Реестр ТС показывает колонку «Эталонный расход, л» рядом с «Нормативный
              расход, л».
            - **AC-5.** Эталонный расход считается тем же алгоритмом веток категорий ТС, что и
              нормативный, с единственной заменой — $H_s$ на $H_эт$ во входе `fuel.q_mileage`
              (и производных).
            """;

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

            Выполнено полностью. Спека и прототип приведены в соответствие: домен, методика,
            backend-модели, три UI-документа и прототип (`shell.js`, `vehicle-card.html`,
            `vehicle-registry.html`, сгенерированный `modification-selection.html`).

            `doc` нашёл и исправлены 3 находки: не заявленная в blast radius связь на
            `EPIC_NtsFuelCalc` (добавлены OQ-25/OQ-26, но сам эпик не упомянут ни в `Links`, ни в
            blast radius — исправлено); голые текстовые упоминания CR/`DOC_AisRefreshPolicy` в
            OQ-24/25/26 вместо typed-ссылок (переведены на `SdaidAnchors.RefTo`); порядок полей
            `ReferenceConsumptionRate`/`ReferenceNormConsumption` в `OutputModel.cs`/`VehicleCalc.cs`
            нарушал правило «итог → вход» (переставлены).

            `testovich` реальным выполнением подтвердил AC-1 (независимость $H_эт$ от
            «Отобранных»), AC-2 (сохранение по «Применить»), AC-3 (размещение полей, прочерк при
            пустой базовой группе), AC-4 (колонка реестра), AC-5 (та же ветка методики,
            пропорциональность входу в линейных ветках). Нашёл реальный баг: $H_эт$ ошибочно
            гасилась в «—» при ЛЮБОЙ ошибке секции `fuel` (дата начала эксплуатации, регион и
            т. п.), хотя по спеке (`fuel.reference_norm`) должна зависеть только от непустоты
            базовой группы — нарушение архитектурного принципа «у каждой секции свой независимый
            валидатор» (`DOC_CalcValidators`). Исправлено в `shell.js`, `computeNorm()`: $H_эт$
            теперь выставляется сразу после вычисления, до общей проверки `err`, и исключена из
            общего сброса `RESULT_FIELDS` при ошибке. Исправление подтверждено повторным реальным
            выполнением на том же ТС (Toyota Camry, `b5e22852-...`, без даты начала эксплуатации):
            $H_эт$ = 4,60 л/100 км вместо «—».
            """;
    }
}