diff --git a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java index 00edaf1..dceebf1 100644 --- a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java +++ b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/BundleManifest.java @@ -25,5 +25,9 @@ public record BundleManifest( String schemaVersion, List supportedLocales, String defaultLocale, - String recordsResource) {} + String recordsResource, + /** Approval Workflow v2 (Phase 3 soak): per-dict opt-in maker-checker. */ + Boolean approvalRequired, + /** Optional Keycloak role для reviewer'а (D1=A approval policy). */ + String approvalMinRole) {} } diff --git a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java index 007a8c3..5517f8d 100644 --- a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java +++ b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleImporter.java @@ -111,6 +111,15 @@ public class CuodBundleImporter { def.setDisplayName(entry.displayName()); def.setSupportedLocales(entryLocales.toArray(String[]::new)); def.setDefaultLocale(entryDefault); + // Approval Workflow v2: bundle-driven opt-in. Только если entry задаёт + // явное значение — иначе оставляем текущее (не сбрасываем UI-toggle'и). + // Honor только когда props.honorApprovalPolicy=true (Phase 3 soak gate). + if (props.honorApprovalPolicy() && entry.approvalRequired() != null) { + def.setApprovalRequired(entry.approvalRequired()); + } + if (props.honorApprovalPolicy() && entry.approvalMinRole() != null) { + def.setApprovalMinRole(entry.approvalMinRole()); + } DictionaryDefinition saved = repository.save(def); var event = new DictionaryDefinitionUpdated( @@ -128,6 +137,29 @@ public class CuodBundleImporter { def = saved; } else { log.debug("Справочник '{}' v{} актуален, schema не трогаем", entry.name(), def.getSchemaVersion()); + // Approval Workflow v2 soak: даже если schema_version неизменилась, + // bundle-driven opt-in требует, чтобы Boolean approvalRequired в + // manifest применился к существующему dict (Phase 3 — flip без bumping + // schema). Apply ТОЛЬКО при изменении — иначе spam UPDATE'ов. Honor + // только когда props.honorApprovalPolicy=true (env-gated). + if (props.honorApprovalPolicy()) { + boolean changed = false; + if (entry.approvalRequired() != null + && entry.approvalRequired() != def.isApprovalRequired()) { + def.setApprovalRequired(entry.approvalRequired()); + changed = true; + } + if (entry.approvalMinRole() != null + && !java.util.Objects.equals(entry.approvalMinRole(), def.getApprovalMinRole())) { + def.setApprovalMinRole(entry.approvalMinRole()); + changed = true; + } + if (changed) { + def = repository.save(def); + log.info("Справочник '{}' approval policy обновлён: required={} role={}", + def.getName(), def.isApprovalRequired(), def.getApprovalMinRole()); + } + } } // Records seed запускаем всегда — они идемпотентны (skip on existing businessKey). importRecords(manifest, entry, def); @@ -142,6 +174,15 @@ public class CuodBundleImporter { fresh.setBundle(manifest.bundle()); fresh.setSupportedLocales(entryLocales.toArray(String[]::new)); fresh.setDefaultLocale(entryDefault); + // Approval Workflow v2: per-dict opt-in через manifest (Phase 3 soak). + // Honor только когда props.honorApprovalPolicy=true — staging opts in, + // prod игнорирует пока явно не включит. + if (props.honorApprovalPolicy() && entry.approvalRequired() != null) { + fresh.setApprovalRequired(entry.approvalRequired()); + } + if (props.honorApprovalPolicy() && entry.approvalMinRole() != null) { + fresh.setApprovalMinRole(entry.approvalMinRole()); + } DictionaryDefinition saved = repository.save(fresh); var event = new DictionaryDefinitionCreated( diff --git a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleProperties.java b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleProperties.java index 8a22e85..a3d5fd3 100644 --- a/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleProperties.java +++ b/ordinis-cuod-bundle/src/main/java/cloud/nstart/terravault/ordinis/cuod/CuodBundleProperties.java @@ -6,7 +6,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public record CuodBundleProperties( boolean autoImport, boolean updateExistingSchema, - String resourceRoot) { + String resourceRoot, + /** + * Approval Workflow v2 (Phase 3 soak): применять {@code approvalRequired} / + * {@code approvalMinRole} из manifest.json при импорте. Default {@code false} + * — bundle ship'ит флаги для staging, prod игнорирует пока не явно включат. + */ + boolean honorApprovalPolicy) { public CuodBundleProperties { if (resourceRoot == null) resourceRoot = "classpath:/bundles/cuod/"; diff --git a/ordinis-cuod-bundle/src/main/resources/bundles/cuod/manifest.json b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/manifest.json index 240feb8..934b154 100644 --- a/ordinis-cuod-bundle/src/main/resources/bundles/cuod/manifest.json +++ b/ordinis-cuod-bundle/src/main/resources/bundles/cuod/manifest.json @@ -2,14 +2,14 @@ "bundle": "cuod", "displayName": "ЦУОД ДЗЗ", "description": "Bundle справочников для Центра Управления Обработкой Данных ДЗЗ. Anchor v1.", - "version": "1.3.0", + "version": "1.3.1", "supportedLocales": ["ru-RU", "en-US"], "defaultLocale": "ru-RU", "dictionaries": [ - { "name": "spacecraft", "displayName": "Космические аппараты", "description": "Каталог КА (космических аппаратов) ДЗЗ-миссий", "scope": "PUBLIC", "schemaResource": "spacecraft.schema.json", "schemaVersion": "1.1.0" }, + { "name": "spacecraft", "displayName": "Космические аппараты", "description": "Каталог КА (космических аппаратов) ДЗЗ-миссий", "scope": "PUBLIC", "schemaResource": "spacecraft.schema.json", "schemaVersion": "1.1.0", "approvalRequired": true }, { "name": "satellite_type", "displayName": "Типы КА", "description": "Классификация типов космических аппаратов по миссиям и характеристикам", "scope": "PUBLIC", "schemaResource": "satellite_type.schema.json", "schemaVersion": "1.0.0" }, { "name": "spacecraft_status", "displayName": "Статусы КА", "description": "Жизненный цикл КА: planned → operational → decommissioned/lost.", "scope": "PUBLIC", "schemaResource": "spacecraft_status.schema.json", "schemaVersion": "1.0.0" }, - { "name": "ground_station", "displayName": "Наземные станции", "description": "Каталог наземных пунктов приёма и управления", "scope": "PUBLIC", "schemaResource": "ground_station.schema.json", "schemaVersion": "1.0.0" }, + { "name": "ground_station", "displayName": "Наземные станции", "description": "Каталог наземных пунктов приёма и управления", "scope": "PUBLIC", "schemaResource": "ground_station.schema.json", "schemaVersion": "1.0.0", "approvalRequired": true }, { "name": "antenna", "displayName": "Антенны", "description": "Антенны наземных станций с G/T и поддерживаемыми диапазонами.", "scope": "PUBLIC", "schemaResource": "antenna.schema.json", "schemaVersion": "1.0.0" }, { "name": "mission", "displayName": "Миссии / программы", "description": "Программы ДЗЗ — Ресурс-П, Канопус-В, Sentinel и т.д. Группируют семейства КА.", "scope": "PUBLIC", "schemaResource": "mission.schema.json", "schemaVersion": "1.0.0" }, { "name": "instrument", "displayName": "Инструменты съёмки", "description": "Полезная нагрузка / сенсоры на борту КА (Геотон-Л1, MODIS, C-SAR, ...)", "scope": "PUBLIC", "schemaResource": "instrument.schema.json", "schemaVersion": "1.0.0" },