"""Настройки модуля jira: умолчания, ключи для агента, проверка значений, перенос из config.example.json.""" from ...core import settings as core_settings from . import projects DEFAULT_PACING = {"ensure_issue": 120, "ensure_fields": 120, "ensure_comment": 30, "ensure_status": 30, "ensure_link": 30, "ensure_sprint": 30, "ensure_attachment": 30, "fetch_attachment": 2, "refresh": 2, "ensure_sprint_meta": 30, "ensure_sprint_state": 30, "ensure_rank": 30} DEFAULTS = { "min_agent_version": 1, "project_key": "AURORA", "pacing": DEFAULT_PACING, "auto_refresh_minutes": 60, "issue_types": {}, "epic_name_field_id": None, "field_ids": {}, "epic_link": {}, "priority_map": {}, "priority_value_key": "name", "labels": [], "kind_labels": {}, "comment_marker": "", "link_types": {}, "closed_statuses": ["Готово", "Снято"], # статусы Jira закрытых задач: список и доска приложения (hide-closed) } # Ключи прежних версий (fact-status-removal; base_jql и board_id — v3, #129): их удаляет миграция v8 и каждый ensure_defaults — процесс со старым кодом # (MCP-сессия до reconnect) вставил бы их снова своими умолчаниями OBSOLETE = ("status_labels", "transitions_map", "base_jql", "board_id") # Что получает агент от модуля (без токенов и служебных ключей хаба); settings_version добавляет ядро AGENT_KEYS = ["project_key", "issue_types", "epic_name_field_id", "field_ids", "epic_link", "priority_map", "priority_value_key", "labels", "kind_labels", "comment_marker", "link_types", "pacing", "min_agent_version"] def _is_int(v) -> bool: return isinstance(v, int) and not isinstance(v, bool) def validate_setting(key, value) -> bool: """True — ключ принадлежит модулю и проверен; ValueError — значение плохое (422 в API). base_jql и board_id не пишутся: JQL собирается из проектов, доска — свойство проекта; pacing — объект {тип команды: целое >= 0} только с известными типами команд; auto_refresh_minutes и min_agent_version — целые >= 0.""" if key == "base_jql": raise ValueError("базовая JQL собирается из проектов (GET /projects)") if key == "board_id": raise ValueError("доска Jira — свойство проекта (POST /projects)") if key == "pacing": if not isinstance(value, dict): raise ValueError("pacing — объект {тип команды: пауза в секундах}") for ctype, pause in value.items(): if ctype not in DEFAULT_PACING: raise ValueError(f"pacing: неизвестный тип команды «{ctype}»") if not _is_int(pause) or pause < 0: raise ValueError(f"pacing.{ctype}: ожидается целое >= 0") return True if key in ("auto_refresh_minutes", "min_agent_version"): if not _is_int(value) or value < 0: raise ValueError(f"{key} — целое >= 0") return True return False def settings_view(conn) -> dict: """Вычисляемые поля GET/PUT /settings.""" return {"base_jql": projects.base_jql(conn)} def pacing(conn) -> dict: return {**DEFAULT_PACING, **(core_settings.get(conn, "pacing") or {})} def pacing_from_config(p: dict) -> dict: """Старая форма pacing (issue_seconds / comment_seconds) → паузы по типам команд.""" issue, comment = p.get("issue_seconds", 120), p.get("comment_seconds", 30) return {"ensure_issue": issue, "ensure_fields": issue, "ensure_comment": comment, "ensure_status": comment, "ensure_link": comment, "ensure_sprint": comment, "ensure_attachment": comment, "fetch_attachment": 2, "refresh": 2, "ensure_sprint_meta": comment, "ensure_sprint_state": comment, "ensure_rank": comment} def _issue_types(cfg: dict) -> dict: """issue_types из ключей конфига issue_type_<тип>[_id]: task и epic — всегда, bug — только если в конфиге есть issue_type_bug или issue_type_bug_id (bug-and-attachments, data-model «Настройки»).""" out = {t: {"name": cfg.get(f"issue_type_{t}"), "id": cfg.get(f"issue_type_{t}_id")} for t in ("task", "epic")} if "issue_type_bug" in cfg or "issue_type_bug_id" in cfg: out["bug"] = {"name": cfg.get("issue_type_bug"), "id": cfg.get("issue_type_bug_id")} return out def migrate_v7(conn) -> None: """Шаг миграции v7 → v8 (fact-status-removal): колонка issue_note.fact_status и ключи status_labels, transitions_map удаляются безвозвратно; settings_version поднимается, если удалён хотя бы один ключ и строка версии есть.""" if "fact_status" in {r["name"] for r in conn.execute("PRAGMA table_info(issue_note)")}: conn.execute("ALTER TABLE issue_note DROP COLUMN fact_status") gone = conn.execute(f"DELETE FROM setting WHERE key IN ({', '.join('?' * len(OBSOLETE))})", OBSOLETE).rowcount if gone: conn.execute("UPDATE setting SET value = CAST(value AS INTEGER) + 1 WHERE key = 'settings_version'") def init_from_config(conn, cfg: dict, force=False) -> list[str]: """Перенести из config.example.json всё, что относится к проекту Jira. Учётные данные и адрес Jira (auth, base_url, verify_ssl, ca_bundle) остаются у агента и сюда не попадают; status_labels и transitions — тоже (статуса по факту в хабе нет, fact-status-removal).""" if core_settings.get(conn, "initialized") and not force: raise RuntimeError("хаб уже инициализирован; повторный init — с --force") project = cfg["project_key"] values = { "project_key": project, "issue_types": _issue_types(cfg), "epic_name_field_id": cfg.get("epic_name_field_id"), "field_ids": cfg.get("fields", {}), "epic_link": cfg.get("epic_link", {}), "priority_map": cfg.get("priority_map", {}), "priority_value_key": cfg.get("priority_value_key", "name"), "labels": cfg.get("labels", []), "kind_labels": cfg.get("kind_labels", {}), "comment_marker": cfg.get("comment_marker", ""), "link_types": cfg.get("link_types", {}), "pacing": pacing_from_config(cfg.get("pacing", {})), } for key, value in values.items(): core_settings.set_(conn, key, value, bump=False) core_settings.set_(conn, "initialized", True) return sorted(values) def on_init(conn, cfg: dict, force: bool) -> list[str]: """init --config: настройки Jira и проект AWPCA; строки для вывода.""" lines = ["перенесено: " + ", ".join(init_from_config(conn, cfg, force=force))] if projects.ensure_legacy(conn, cfg.get("sprints", {}).get("board_id")): board = projects.board_of(conn, projects.LEGACY_LABEL) lines.append(f"проект {projects.LEGACY_LABEL} «{projects.LEGACY_NAME}», доска {board}") return lines