feat(027): Final Phase T038-T043 implementation

- T038: SessionEvent logger and persistence logic
  - Added SessionEventLogger service with explicit audit event persistence
  - Added SessionEvent model with events relationship on DatasetReviewSession
  - Integrated event logging into orchestrator flows and API mutation endpoints

- T039: Semantic source version propagation
  - Added source_version column to SemanticFieldEntry
  - Added propagate_source_version_update() to SemanticResolver
  - Preserves locked/manual field invariants during propagation

- T040: Batch approval API and UI actions
  - Added batch semantic approval endpoint (/fields/semantic/approve-batch)
  - Added batch mapping approval endpoint (/mappings/approve-batch)
  - Added batch approval actions to SemanticLayerReview and ExecutionMappingReview components
  - Aligned batch semantics with single-item approval contracts

- T041: Superset compatibility matrix tests
  - Added test_superset_matrix.py with preview and SQL Lab fallback coverage
  - Tests verify client method preference and matrix fallback behavior

- T042: RBAC audit sweep on session-mutation endpoints
  - Added _require_owner_mutation_scope() helper
  - Applied owner guards to update_session, delete_session, and all mutation endpoints
  - Ensured no bypass of existing permission checks

- T043: i18n coverage for dataset-review UI
  - Added workspace state labels (empty/importing/review) to en.json and ru.json
  - Added batch action labels for semantics and mappings
  - Fixed workspace state comparison to lowercase strings
  - Removed hardcoded workspace state display strings

Signed-off-by: Implementation Specialist <impl@ss-tools>
This commit is contained in:
2026-03-17 14:29:33 +03:00
parent 38bda6a714
commit ed3d5f3039
33 changed files with 99234 additions and 93415 deletions

View File

@@ -664,6 +664,11 @@
"export_failed": "Failed to export the requested review artifact.",
"empty_state_title": "No session loaded",
"state_label": "Workspace",
"state": {
"empty": "Empty",
"importing": "Importing",
"review": "Review"
},
"readiness_label": "Readiness",
"source_badge_fallback": "review session",
"save_session_action": "Save session",
@@ -721,6 +726,8 @@
"preview_compiler_label": "Compiled by",
"preview_pending_note": "Compiled preview is not part of this US1 batch yet, but the workspace keeps the state visible when present.",
"jump_target_label": "Focused area",
"semantic_review_visible": "Semantic review is active for field-level source decisions.",
"clarification_visible": "Clarification is available with one active question at a time.",
"resume_action": "Resume session",
"pause_action": "Pause session",
"actions": {
@@ -765,6 +772,245 @@
"low_confidence": "Low confidence",
"unresolved": "Unresolved"
}
},
"semantics": {
"eyebrow": "Semantic layer",
"title": "Review semantic field values",
"description": "Compare the active semantic value with ranked candidates, preserve provenance, and lock or unlock manual decisions explicitly.",
"field_count_label": "Fields in review",
"empty": "No semantic fields need review right now.",
"unknown_source": "Unknown source",
"active_value_label": "Active semantic value",
"provenance_label": "Provenance",
"confidence_label": "Confidence",
"confidence_unset": "No confidence rank",
"confidence_rank_label": "Rank",
"source_label": "Source",
"changed_by_label": "Changed by",
"locked_badge": "Locked",
"unlocked_badge": "Unlocked",
"conflict_badge": "Conflict",
"needs_review_badge": "Needs review",
"manual_verbose_name_label": "Manual verbose name",
"manual_description_label": "Manual description",
"manual_display_format_label": "Manual display format",
"manual_override_action": "Manual override",
"approve_all_action": "Approve visible candidates",
"save_manual_action": "Save manual value",
"lock_action": "Lock field",
"unlock_action": "Unlock field",
"feedback_up_action": "👍",
"feedback_down_action": "👎",
"candidates_title": "Candidate options",
"candidates_empty": "No ranked candidate options were returned for this field.",
"candidate_description_empty": "No description proposed for this candidate.",
"display_format_label": "Display format",
"score_label": "Score",
"apply_candidate_action": "Apply",
"apply_and_lock_action": "Apply & lock",
"empty_value": "No current value",
"provenance": {
"unresolved": "Unresolved",
"dictionary_exact": "Dictionary exact match",
"reference_imported": "Imported reference",
"ai_generated": "AI generated",
"fuzzy_inferred": "Fuzzy inferred",
"manual_override": "Manual override"
},
"candidate_status": {
"pending": "Pending",
"accepted": "Accepted",
"superseded": "Superseded",
"rejected": "Rejected"
},
"messages": {
"saving": "Saving semantic decision...",
"save_failed": "Failed to save semantic decision.",
"manual_override_required": "Provide at least one manual semantic value before saving.",
"manual_saved": "Manual semantic value saved and locked.",
"candidate_applied": "Candidate value applied.",
"candidate_locked": "Candidate value applied and locked.",
"locked": "Field locked against silent overwrite.",
"unlocked": "Field unlocked for renewed review.",
"feedback_up": "Positive feedback recorded for this semantic value.",
"feedback_down": "Negative feedback recorded for this semantic value."
}
},
"clarification": {
"eyebrow": "Clarification",
"title": "Resolve one ambiguity at a time",
"description": "Answer the active clarification question, skip it, or send it to expert review while progress stays resumable.",
"progress_label": "Clarification progress",
"state_label": "Dialog state",
"status_label": "Status",
"remaining_label": "Remaining questions",
"summary_delta_label": "Summary delta",
"active_question_label": "Active question",
"why_it_matters_label": "Why it matters",
"current_guess_label": "Current guess",
"current_guess_empty": "No current guess is available.",
"topic_label": "Topic reference",
"options_title": "Available options",
"recommended_badge": "Recommended",
"custom_answer_label": "Custom answer",
"answer_action": "Answer with selected option",
"custom_answer_action": "Submit custom answer",
"skip_action": "Skip for now",
"assistant_action": "Open assistant guidance",
"expert_review_action": "Send to expert review",
"resume_action": "Resume clarification",
"completed": "No active clarification question remains. The session can be resumed later if new ambiguity appears.",
"messages": {
"saving": "Saving clarification answer...",
"saved": "Clarification answer saved.",
"skipped": "Question skipped and left resumable.",
"expert_review_requested": "Question escalated to expert review.",
"assistant_opened": "Assistant guidance opened for this clarification step.",
"resumed": "Clarification resumed from the highest-priority unresolved question.",
"resume_failed": "Failed to resume clarification.",
"save_failed": "Failed to save clarification answer.",
"option_required": "Select an option before submitting the answer.",
"custom_required": "Enter a custom answer before submitting."
}
},
"mapping": {
"eyebrow": "Template mapping",
"title": "Review filter-to-template mappings",
"description": "Verify imported filter values, effective execution values, and warning-sensitive transformations before preview or launch.",
"state_label": "Mapping state",
"state": {
"Incomplete": "Incomplete",
"WarningApproval": "Approval required",
"Approved": "Approved"
},
"pending_approvals_label": "Pending approvals",
"required_values_label": "Missing required values",
"empty": "No execution mappings are available yet.",
"required_blockers_notice": "{count} required values still need attention before preview or launch can proceed.",
"approval_notice": "{count} mapping transformations still need explicit approval.",
"to_variable_label": "To variable",
"required_badge": "Required",
"approval_required_badge": "Approval required",
"approved_badge": "Approved",
"source_filter_label": "Source filter",
"source_value_label": "Source value",
"confidence_label": "Confidence",
"recovery_label": "Recovery",
"effective_value_label": "Effective value",
"method_label": "Method",
"approval_label": "Approval",
"warning_label": "Warning",
"transformation_note_label": "Transformation note",
"missing_required_value": "This mapping still lacks a required effective value.",
"manual_value_label": "Manual effective value",
"manual_note_label": "Manual override note",
"save_override_action": "Save manual override",
"manual_override_action": "Manual override",
"approve_action": "Approve mapping",
"approve_all_action": "Approve all pending mappings",
"approval_state": {
"pending": "Pending",
"approved": "Approved",
"rejected": "Rejected",
"not_required": "Not required"
},
"warning_level": {
"none": "None",
"low": "Low",
"medium": "Medium",
"high": "High"
},
"method": {
"direct_match": "Direct match",
"heuristic_match": "Heuristic match",
"semantic_match": "Semantic match",
"manual_override": "Manual override"
},
"messages": {
"saving": "Saving mapping decision...",
"approval_saved": "Mapping approval recorded.",
"approval_failed": "Failed to save mapping approval.",
"override_saved": "Manual mapping override saved. Preview should be refreshed.",
"override_failed": "Failed to save manual mapping override.",
"required_value_missing": "Provide a required value before saving the manual override."
}
},
"preview": {
"eyebrow": "Compiled SQL preview",
"title": "Review Superset-compiled SQL",
"description": "Preview truth comes only from Superset. Regenerate the preview whenever mappings or required inputs change.",
"state_label": "Preview state",
"state": {
"missing": "Missing",
"pending": "Pending",
"ready": "Ready",
"stale": "Stale",
"failed": "Error"
},
"generate_action": "Generate SQL preview",
"generate_loading": "Generating preview...",
"missing_body": "No Superset preview is available yet. Generate one before attempting launch.",
"pending_body": "Superset is compiling the current execution context. Launch stays blocked until preview completes.",
"ready_body": "This SQL preview was compiled by Superset for the current execution inputs.",
"stale_body": "Mappings or effective values changed after the last successful preview. Regenerate before launch.",
"error_body": "Superset could not compile the current execution context.",
"compiler_label": "Compiled source",
"compiled_by_superset": "Compiled by Superset",
"compiled_source_unknown": "Compilation source unavailable",
"fingerprint_label": "Preview fingerprint",
"compiled_at_label": "Compiled at",
"sql_block_title": "Superset SQL",
"compiled_truth_note": "Exact SQL returned by Superset",
"go_to_mapping_action": "Review mapping inputs",
"review_inputs_action": "Review changed inputs",
"messages": {
"generated": "Superset preview refreshed.",
"generate_failed": "Failed to generate Superset preview."
}
},
"launch": {
"eyebrow": "Launch confirmation",
"title": "Confirm SQL Lab launch",
"description": "Launch remains blocked until preview truth, approvals, and readiness gates all match the reviewed execution context.",
"state_label": "Launch state",
"state": {
"Blocked": "Blocked",
"Ready": "Ready",
"Submitted": "Submitted"
},
"blocked_title": "Launch blockers",
"blocked_body": "Resolve the items below before sending this dataset run to SQL Lab.",
"resolve_action": "Open related area",
"dataset_ref_label": "Dataset reference",
"readiness_label": "Readiness",
"approved_mappings_label": "Approved mappings",
"preview_fingerprint_label": "Preview fingerprint",
"sql_lab_target_title": "Launch target",
"sql_lab_target_body": "The canonical launch target is a Superset SQL Lab session using the reviewed preview and effective execution inputs.",
"preview_status_label": "Preview status",
"compiled_by_label": "Compiled by",
"launch_action": "Launch dataset",
"launch_loading": "Launching dataset...",
"submitted_title": "Launch submitted",
"submitted_body": "SQL Lab handoff and audited run context were recorded for this launch request.",
"run_context_label": "Run context",
"sql_lab_session_label": "SQL Lab session",
"launch_status_label": "Launch status",
"preview_ref_label": "Preview reference",
"blockers": {
"blocking_finding": "Blocking findings remain unresolved",
"mapping_approval_required": "Mapping approval is still required",
"preview_missing": "Superset preview is required before launch",
"preview_pending": "Preview generation is still in progress",
"preview_stale": "Preview no longer matches the current execution inputs",
"preview_failed": "Preview failed and launch remains blocked",
"readiness_not_run_ready": "Session is not yet in run-ready state",
"preview_fingerprint_missing": "Preview fingerprint is missing, so launch cannot be trusted"
},
"messages": {
"launch_started": "Dataset launch request sent to SQL Lab.",
"launch_failed": "Failed to launch dataset in SQL Lab."
}
}
},
"tasks": {

View File

@@ -662,6 +662,11 @@
"export_failed": "Не удалось экспортировать выбранный артефакт review.",
"empty_state_title": "Сессия не загружена",
"state_label": "Workspace",
"state": {
"empty": "Пусто",
"importing": "Импорт",
"review": "Проверка"
},
"readiness_label": "Готовность",
"source_badge_fallback": "review-сессия",
"save_session_action": "Сохранить сессию",
@@ -719,6 +724,8 @@
"preview_compiler_label": "Скомпилировано",
"preview_pending_note": "Скомпилированный preview не входит в этот пакет US1, но workspace сохраняет его видимым, если он уже существует.",
"jump_target_label": "Выбранная зона",
"semantic_review_visible": "Семантический review активен для полей с решениями по источникам.",
"clarification_visible": "Уточнение доступно в режиме одного активного вопроса.",
"resume_action": "Возобновить сессию",
"pause_action": "Поставить сессию на паузу",
"actions": {
@@ -763,6 +770,245 @@
"low_confidence": "Низкая уверенность",
"unresolved": "Не разрешено"
}
},
"semantics": {
"eyebrow": "Семантический слой",
"title": "Проверьте семантические значения полей",
"description": "Сравнивайте активное семантическое значение с ранжированными кандидатами, сохраняйте provenance и явно блокируйте или разблокируйте ручные решения.",
"field_count_label": "Поля в review",
"empty": "Семантические поля сейчас не требуют review.",
"unknown_source": "Неизвестный источник",
"active_value_label": "Активное семантическое значение",
"provenance_label": "Provenance",
"confidence_label": "Уверенность",
"confidence_unset": "Ранг уверенности отсутствует",
"confidence_rank_label": "Ранг",
"source_label": "Источник",
"changed_by_label": "Изменил",
"locked_badge": "Заблокировано",
"unlocked_badge": "Разблокировано",
"conflict_badge": "Конфликт",
"needs_review_badge": "Нужен review",
"manual_verbose_name_label": "Ручное verbose name",
"manual_description_label": "Ручное описание",
"manual_display_format_label": "Ручной display format",
"manual_override_action": "Ручное переопределение",
"approve_all_action": "Подтвердить видимые кандидаты",
"save_manual_action": "Сохранить ручное значение",
"lock_action": "Заблокировать поле",
"unlock_action": "Разблокировать поле",
"feedback_up_action": "👍",
"feedback_down_action": "👎",
"candidates_title": "Варианты кандидатов",
"candidates_empty": "Для этого поля не возвращены ранжированные кандидаты.",
"candidate_description_empty": "Для этого кандидата описание не предложено.",
"display_format_label": "Формат отображения",
"score_label": "Оценка",
"apply_candidate_action": "Применить",
"apply_and_lock_action": "Применить и заблокировать",
"empty_value": "Текущее значение отсутствует",
"provenance": {
"unresolved": "Не разрешено",
"dictionary_exact": "Точное совпадение словаря",
"reference_imported": "Импортированная ссылка",
"ai_generated": "Сгенерировано AI",
"fuzzy_inferred": "Нечеткий вывод",
"manual_override": "Ручное переопределение"
},
"candidate_status": {
"pending": "В ожидании",
"accepted": "Принято",
"superseded": "Заменено",
"rejected": "Отклонено"
},
"messages": {
"saving": "Сохранение семантического решения...",
"save_failed": "Не удалось сохранить семантическое решение.",
"manual_override_required": "Перед сохранением укажите хотя бы одно ручное семантическое значение.",
"manual_saved": "Ручное семантическое значение сохранено и заблокировано.",
"candidate_applied": "Значение кандидата применено.",
"candidate_locked": "Значение кандидата применено и заблокировано.",
"locked": "Поле заблокировано от тихой перезаписи.",
"unlocked": "Поле разблокировано для повторного review.",
"feedback_up": "Позитивный feedback для этого семантического значения сохранен.",
"feedback_down": "Негативный feedback для этого семантического значения сохранен."
}
},
"clarification": {
"eyebrow": "Уточнение",
"title": "Разрешайте неоднозначности по одному вопросу",
"description": "Ответьте на активный вопрос уточнения, пропустите его или отправьте на экспертный review, сохраняя возможность возобновления.",
"progress_label": "Прогресс уточнения",
"state_label": "Состояние диалога",
"status_label": "Статус",
"remaining_label": "Оставшиеся вопросы",
"summary_delta_label": "Изменение summary",
"active_question_label": "Активный вопрос",
"why_it_matters_label": "Почему это важно",
"current_guess_label": "Текущая гипотеза",
"current_guess_empty": "Текущая гипотеза отсутствует.",
"topic_label": "Ссылка на тему",
"options_title": "Доступные варианты",
"recommended_badge": "Рекомендовано",
"custom_answer_label": "Свой ответ",
"answer_action": "Ответить выбранным вариантом",
"custom_answer_action": "Отправить свой ответ",
"skip_action": "Пропустить пока",
"assistant_action": "Открыть помощь ассистента",
"expert_review_action": "Передать на экспертный review",
"resume_action": "Возобновить уточнение",
"completed": "Активных вопросов уточнения не осталось. Сессию можно возобновить позже, если появится новая неоднозначность.",
"messages": {
"saving": "Сохранение ответа на уточнение...",
"saved": "Ответ на уточнение сохранен.",
"skipped": "Вопрос пропущен и сохранен как возобновляемый.",
"expert_review_requested": "Вопрос отправлен на экспертный review.",
"assistant_opened": "Для этого шага уточнения открыта помощь ассистента.",
"resumed": "Уточнение возобновлено с самого приоритетного неразрешенного вопроса.",
"resume_failed": "Не удалось возобновить уточнение.",
"save_failed": "Не удалось сохранить ответ на уточнение.",
"option_required": "Перед отправкой выберите один вариант.",
"custom_required": "Перед отправкой введите свой ответ."
}
},
"mapping": {
"eyebrow": "Маппинг шаблонов",
"title": "Проверьте маппинг фильтров и шаблонов",
"description": "Проверьте импортированные значения фильтров, итоговые execution values и warning-преобразования перед preview или запуском.",
"state_label": "Состояние маппинга",
"state": {
"Incomplete": "Неполный",
"WarningApproval": "Требуется подтверждение",
"Approved": "Подтверждено"
},
"pending_approvals_label": "Ожидают подтверждения",
"required_values_label": "Отсутствуют обязательные значения",
"empty": "Execution mappings пока отсутствуют.",
"required_blockers_notice": "Еще {count} обязательных значений требуют внимания перед preview или запуском.",
"approval_notice": "Еще {count} преобразований маппинга требуют явного подтверждения.",
"to_variable_label": "В переменную",
"required_badge": "Обязательно",
"approval_required_badge": "Нужно подтверждение",
"approved_badge": "Подтверждено",
"source_filter_label": "Исходный фильтр",
"source_value_label": "Исходное значение",
"confidence_label": "Уверенность",
"recovery_label": "Восстановление",
"effective_value_label": "Итоговое значение",
"method_label": "Метод",
"approval_label": "Подтверждение",
"warning_label": "Предупреждение",
"transformation_note_label": "Комментарий к преобразованию",
"missing_required_value": "Для этого маппинга все еще отсутствует обязательное итоговое значение.",
"manual_value_label": "Ручное итоговое значение",
"manual_note_label": "Комментарий к ручному переопределению",
"save_override_action": "Сохранить ручное переопределение",
"manual_override_action": "Ручное переопределение",
"approve_action": "Подтвердить маппинг",
"approve_all_action": "Подтвердить все ожидающие маппинги",
"approval_state": {
"pending": "Ожидает",
"approved": "Подтверждено",
"rejected": "Отклонено",
"not_required": "Не требуется"
},
"warning_level": {
"none": "Нет",
"low": "Низкий",
"medium": "Средний",
"high": "Высокий"
},
"method": {
"direct_match": "Прямое совпадение",
"heuristic_match": "Эвристическое совпадение",
"semantic_match": "Семантическое совпадение",
"manual_override": "Ручное переопределение"
},
"messages": {
"saving": "Сохранение решения по маппингу...",
"approval_saved": "Подтверждение маппинга сохранено.",
"approval_failed": "Не удалось сохранить подтверждение маппинга.",
"override_saved": "Ручное переопределение маппинга сохранено. Preview нужно обновить.",
"override_failed": "Не удалось сохранить ручное переопределение маппинга.",
"required_value_missing": "Перед сохранением ручного переопределения укажите обязательное значение."
}
},
"preview": {
"eyebrow": "Скомпилированный SQL preview",
"title": "Проверьте SQL, скомпилированный Superset",
"description": "Истина preview приходит только от Superset. Перегенерируйте preview после изменений маппинга или обязательных входных значений.",
"state_label": "Состояние preview",
"state": {
"missing": "Отсутствует",
"pending": "Генерируется",
"ready": "Готов",
"stale": "Устарел",
"failed": "Ошибка"
},
"generate_action": "Сгенерировать SQL preview",
"generate_loading": "Генерация preview...",
"missing_body": "Preview от Superset пока отсутствует. Сгенерируйте его перед запуском.",
"pending_body": "Superset компилирует текущий execution context. Запуск остается заблокированным до завершения preview.",
"ready_body": "Этот SQL preview был скомпилирован Superset для текущих execution inputs.",
"stale_body": "После последнего успешного preview были изменены маппинги или итоговые значения. Перед запуском нужно перегенерировать preview.",
"error_body": "Superset не смог скомпилировать текущий execution context.",
"compiler_label": "Источник компиляции",
"compiled_by_superset": "Скомпилировано Superset",
"compiled_source_unknown": "Источник компиляции недоступен",
"fingerprint_label": "Fingerprint preview",
"compiled_at_label": "Время компиляции",
"sql_block_title": "SQL от Superset",
"compiled_truth_note": "Точный SQL, возвращенный Superset",
"go_to_mapping_action": "Проверить mapping inputs",
"review_inputs_action": "Проверить измененные inputs",
"messages": {
"generated": "Preview от Superset обновлен.",
"generate_failed": "Не удалось сгенерировать preview от Superset."
}
},
"launch": {
"eyebrow": "Подтверждение запуска",
"title": "Подтвердите запуск в SQL Lab",
"description": "Запуск остается заблокированным, пока preview, approvals и readiness gates не соответствуют проверенному execution context.",
"state_label": "Состояние запуска",
"state": {
"Blocked": "Заблокирован",
"Ready": "Готов",
"Submitted": "Отправлен"
},
"blocked_title": "Блокеры запуска",
"blocked_body": "Разрешите следующие пункты перед отправкой запуска датасета в SQL Lab.",
"resolve_action": "Открыть связанный раздел",
"dataset_ref_label": "Ссылка на датасет",
"readiness_label": "Готовность",
"approved_mappings_label": "Подтвержденные маппинги",
"preview_fingerprint_label": "Fingerprint preview",
"sql_lab_target_title": "Цель запуска",
"sql_lab_target_body": "Каноническая цель запуска — сессия Superset SQL Lab с использованием проверенного preview и итоговых execution inputs.",
"preview_status_label": "Статус preview",
"compiled_by_label": "Скомпилировано",
"launch_action": "Запустить датасет",
"launch_loading": "Запуск датасета...",
"submitted_title": "Запуск отправлен",
"submitted_body": "Передача в SQL Lab и аудитируемый run context были записаны для этого запроса запуска.",
"run_context_label": "Run context",
"sql_lab_session_label": "Сессия SQL Lab",
"launch_status_label": "Статус запуска",
"preview_ref_label": "Ссылка на preview",
"blockers": {
"blocking_finding": "Есть неразрешенные блокирующие findings",
"mapping_approval_required": "Маппинг все еще требует подтверждения",
"preview_missing": "Перед запуском требуется preview от Superset",
"preview_pending": "Preview все еще генерируется",
"preview_stale": "Preview больше не соответствует текущим execution inputs",
"preview_failed": "Preview завершился ошибкой, запуск заблокирован",
"readiness_not_run_ready": "Сессия еще не находится в состоянии run-ready",
"preview_fingerprint_missing": "Fingerprint preview отсутствует, поэтому запуску нельзя доверять"
},
"messages": {
"launch_started": "Запрос запуска датасета отправлен в SQL Lab.",
"launch_failed": "Не удалось запустить датасет в SQL Lab."
}
}
},
"tasks": {