diff --git a/agents/prompts/knowledge_synthesizer.md b/agents/prompts/knowledge_synthesizer.md deleted file mode 100644 index 28e824b..0000000 --- a/agents/prompts/knowledge_synthesizer.md +++ /dev/null @@ -1,93 +0,0 @@ -You are a Knowledge Synthesizer for the Kin multi-agent orchestrator. - -Your job: aggregate and synthesize outputs from multiple parallel research agents into a unified, confidence-rated knowledge base ready for the Architect. - -## Input - -You receive: -- PROJECT: id, name, path, tech stack -- TASK: id, title, brief with `phases_context` keyed by researcher role name (e.g. `business_analyst`, `market_researcher`) -- DECISIONS: known architectural decisions and conventions -- PREVIOUS STEP OUTPUT: latest approved researcher outputs (if phases_context is absent) - -## Working Mode - -1. Read `brief.phases_context` — each key is a researcher role, value is their output summary -2. Identify consensus findings: claims supported by ≥2 researcher roles -3. Identify conflicts: directly contradictory findings between roles; name the conflict explicitly -4. Assign confidence rating to each conclusion: `high` (≥2 supporting roles), `medium` (1 role), `low` (inferred) -5. Prioritize actions: order by impact and confidence, not by which role reported them first - -## Focus On - -- Synthesize, do not repeat raw data — extract conclusions, not summaries of summaries -- Make conflicts explicit: name both positions and which roles hold them; do not paper over disagreements -- Confidence must be grounded in cross-role agreement, not assertion strength -- Unresolved conflicts = honest knowledge gaps, not speculation — flag them as open questions -- `phases_context_used` must list every input role to prove completeness - -## Quality Checks - -- Every item in `confidence_rated_conclusions` cites at least one `supporting_roles` entry -- `unified_findings` contains no raw data dumps — only synthesized insights -- `unresolved_conflicts` is not silently omitted when contradictions exist -- `prioritized_actions` are concrete and directed at the Architect, not vague recommendations -- All input roles from `phases_context` appear in `phases_context_used` - -## Return Format - -Return ONLY valid JSON (no markdown, no explanation): - -```json -{ - "status": "done", - "unified_findings": [ - "Synthesized insight that multiple researchers converge on" - ], - "confidence_rated_conclusions": [ - { - "conclusion": "...", - "confidence": "high | medium | low", - "supporting_roles": ["business_analyst", "market_researcher"], - "rationale": "Why this confidence level" - } - ], - "unresolved_conflicts": [ - { - "topic": "...", - "positions": { - "role_a": "their position", - "role_b": "their position" - }, - "recommendation": "What the Architect should decide" - } - ], - "prioritized_actions": [ - "Action 1 for Architect — grounded in high-confidence findings", - "Action 2 for Architect" - ], - "phases_context_used": ["business_analyst", "market_researcher"] -} -``` - -Valid values for `status`: `"done"`, `"blocked"`. - -If status is "blocked", include `"blocked_reason": "..."`. - -## Constraints - -- Do NOT repeat raw researcher outputs — synthesize only -- Do NOT assign `confidence: high` to findings supported by only one role -- Do NOT omit `unresolved_conflicts` when contradictions exist between roles -- Do NOT write implementation code or architectural decisions — that is the Architect's job -- Do NOT speculate beyond what researchers reported; unknown = unresolved - -## Blocked Protocol - -If you cannot perform the task (phases_context is missing or empty, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output: - -```json -{"status": "blocked", "reason": "", "blocked_at": ""} -``` - -Use current datetime for `blocked_at`. Do NOT guess or partially complete — return blocked immediately. diff --git a/agents/specialists.yaml b/agents/specialists.yaml index cd3af24..6ec24b4 100644 --- a/agents/specialists.yaml +++ b/agents/specialists.yaml @@ -227,21 +227,6 @@ specialists: context_rules: decisions: all - knowledge_synthesizer: - name: "Knowledge Synthesizer" - model: sonnet - tools: [Read, Grep, Glob] - description: "Aggregates multi-agent research outputs into unified, confidence-rated knowledge base for the Architect" - permissions: read_only - context_rules: - decisions: all - output_schema: - unified_findings: "array of strings" - confidence_rated_conclusions: "array of { conclusion, confidence: high|medium|low, supporting_roles, rationale }" - unresolved_conflicts: "array of { topic, positions: { role: position }, recommendation }" - prioritized_actions: "array of strings" - phases_context_used: "array of role names" - research_head: name: "Research Department Head" model: opus diff --git a/core/context_builder.py b/core/context_builder.py index 8d99668..f53d2f5 100644 --- a/core/context_builder.py +++ b/core/context_builder.py @@ -75,9 +75,6 @@ def build_context( ctx["modules"] = models.get_modules(conn, project_id) ctx["decisions"] = models.get_decisions(conn, project_id) - elif role == "knowledge_synthesizer": - ctx["decisions"] = models.get_decisions(conn, project_id) - elif role == "debugger": ctx["decisions"] = models.get_decisions( conn, project_id, types=["gotcha", "workaround"], diff --git a/core/phases.py b/core/phases.py index 73026e3..1e08bac 100644 --- a/core/phases.py +++ b/core/phases.py @@ -10,7 +10,7 @@ import sqlite3 from core import models -# Canonical order of research roles (knowledge_synthesizer and architect always last) +# Canonical order of research roles (architect always last) RESEARCH_ROLES = [ "business_analyst", "market_researcher", @@ -18,7 +18,6 @@ RESEARCH_ROLES = [ "tech_researcher", "ux_designer", "marketer", - "knowledge_synthesizer", "architect", ] @@ -30,18 +29,17 @@ ROLE_LABELS = { "tech_researcher": "Tech Researcher", "ux_designer": "UX Designer", "marketer": "Marketer", - "knowledge_synthesizer": "Knowledge Synthesizer", "architect": "Architect", } def validate_roles(roles: list[str]) -> list[str]: - """Filter unknown roles, remove duplicates, strip auto-managed roles (architect, knowledge_synthesizer).""" + """Filter unknown roles, remove duplicates, strip 'architect' (auto-added later).""" seen: set[str] = set() result = [] for r in roles: r = r.strip().lower() - if r in ("architect", "knowledge_synthesizer"): + if r == "architect": continue if r in RESEARCH_ROLES and r not in seen: seen.add(r) @@ -50,18 +48,9 @@ def validate_roles(roles: list[str]) -> list[str]: def build_phase_order(selected_roles: list[str]) -> list[str]: - """Return roles in canonical RESEARCH_ROLES order. - - Auto-inserts knowledge_synthesizer before architect when ≥2 researchers selected. - Architect always appended last when any researcher is selected. - """ - ordered = [ - r for r in RESEARCH_ROLES - if r in selected_roles and r not in ("architect", "knowledge_synthesizer") - ] + """Return roles in canonical RESEARCH_ROLES order, append architect if any selected.""" + ordered = [r for r in RESEARCH_ROLES if r in selected_roles and r != "architect"] if ordered: - if len(ordered) >= 2: - ordered.append("knowledge_synthesizer") ordered.append("architect") return ordered @@ -87,10 +76,6 @@ def create_project_with_phases( if not ordered_roles: raise ValueError("At least one research role must be selected") - # knowledge_synthesizer is included in build_phase_order output for routing/context, - # but is not yet a pipeline phase — it activates via separate aggregation trigger - pipeline_roles = [r for r in ordered_roles if r != "knowledge_synthesizer"] - project = models.create_project( conn, id, name, path, tech_stack=tech_stack, priority=priority, language=language, @@ -98,7 +83,7 @@ def create_project_with_phases( ) phases = [] - for idx, role in enumerate(pipeline_roles): + for idx, role in enumerate(ordered_roles): phase = models.create_phase(conn, id, role, idx) phases.append(phase) @@ -129,24 +114,6 @@ def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict: "phase_order": phase["phase_order"], "workflow": "research", } - - # knowledge_synthesizer: collect approved researcher outputs into phases_context - if phase["role"] == "knowledge_synthesizer": - all_phases = models.list_phases(conn, phase["project_id"]) - phases_context = {} - for p in all_phases: - if p["status"] == "approved" and p.get("task_id"): - row = conn.execute( - """SELECT output_summary FROM agent_logs - WHERE task_id = ? AND success = 1 - ORDER BY created_at DESC LIMIT 1""", - (p["task_id"],), - ).fetchone() - if row and row["output_summary"]: - phases_context[p["role"]] = row["output_summary"] - if phases_context: - brief["phases_context"] = phases_context - task = models.create_task( conn, task_id, phase["project_id"], title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}", diff --git a/tests/test_kin_docs_002_regression.py b/tests/test_kin_docs_002_regression.py index 7f8124a..dafd7d1 100644 --- a/tests/test_kin_docs_002_regression.py +++ b/tests/test_kin_docs_002_regression.py @@ -115,11 +115,11 @@ class TestAllPromptsContainStandardStructure: class TestPromptCount: """Проверяет, что число промптов не изменилось неожиданно.""" - def test_prompt_count_is_26(self): - """В agents/prompts/ ровно 26 файлов .md.""" + def test_prompt_count_is_25(self): + """В agents/prompts/ ровно 25 файлов .md.""" count = len(_prompt_files()) - assert count == 26, ( # 26 промптов — актуально на 2026-03-19, +knowledge_synthesizer (KIN-DOCS-003, см. git log agents/prompts/) - f"Ожидалось 26 промптов, найдено {count}. " + assert count == 25, ( # 25 промптов — актуально на 2026-03-19 (см. git log agents/prompts/) + f"Ожидалось 25 промптов, найдено {count}. " "Если добавлен новый промпт — обнови этот тест." ) diff --git a/tests/test_kin_docs_003_regression.py b/tests/test_kin_docs_003_regression.py deleted file mode 100644 index d89f8af..0000000 --- a/tests/test_kin_docs_003_regression.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Regression tests for KIN-DOCS-003 — knowledge_synthesizer role. - -Acceptance criteria: -1. Структурный тест промпта knowledge_synthesizer: 5 обязательных секций + специфические поля (#940) -2. Тест регистрации роли в specialists.yaml (model, tools, permissions, output_schema) -3. Тест корректности pipeline research_head с aggregation шагом: - - build_phase_order с ≥2 исследователями вставляет knowledge_synthesizer перед architect - - create_project_with_phases НЕ создаёт knowledge_synthesizer как фазу (авто-управляемая роль) -4. Тест context_builder: knowledge_synthesizer получает decisions, но не modules -5. Тест activate_phase: при ≥2 одобренных фазах инжектирует phases_context в brief -6. Проверка exclusion-листов (#921, #917): knowledge_synthesizer.md не исключён из проверок структуры -""" - -from pathlib import Path -from unittest.mock import patch - -import pytest -import yaml - -from core.db import init_db -from core import models -from core.phases import build_phase_order, create_project_with_phases, activate_phase -from core.context_builder import build_context - -SPECIALISTS_YAML = Path(__file__).parent.parent / "agents" / "specialists.yaml" -PROMPTS_DIR = Path(__file__).parent.parent / "agents" / "prompts" - -REQUIRED_SECTIONS = [ - "## Working Mode", - "## Focus On", - "## Quality Checks", - "## Return Format", - "## Constraints", -] - -OUTPUT_SCHEMA_FIELDS = [ - "unified_findings", - "confidence_rated_conclusions", - "unresolved_conflicts", - "prioritized_actions", - "phases_context_used", -] - - -def _load_yaml(): - return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8")) - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - -@pytest.fixture -def conn(): - c = init_db(":memory:") - yield c - c.close() - - -@pytest.fixture -def conn_with_project(conn): - models.create_project(conn, "p1", "TestProject", "/p1", tech_stack=["python"]) - models.create_task(conn, "P1-001", "p1", "Research task") - yield conn - - -# =========================================================================== -# 1. Структурный тест промпта (#940) -# =========================================================================== - -class TestKnowledgeSynthesizerPrompt: - """Структурный тест agents/prompts/knowledge_synthesizer.md.""" - - def test_prompt_file_exists(self): - """Файл agents/prompts/knowledge_synthesizer.md существует.""" - path = PROMPTS_DIR / "knowledge_synthesizer.md" - assert path.exists(), "knowledge_synthesizer.md не найден в agents/prompts/" - - @pytest.mark.parametrize("section", REQUIRED_SECTIONS) - def test_prompt_has_required_section(self, section): - """Промпт содержит все 5 обязательных секций (REQUIRED_SECTIONS).""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert section in content, ( - f"knowledge_synthesizer.md не содержит обязательную секцию {section!r}" - ) - - def test_prompt_has_input_section(self): - """Промпт содержит секцию ## Input — агент-специфичная секция.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "## Input" in content, ( - "knowledge_synthesizer.md не содержит секцию '## Input'" - ) - - def test_prompt_contains_confidence_rated_conclusions(self): - """Промпт содержит поле confidence_rated_conclusions в Return Format.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "confidence_rated_conclusions" in content, ( - "knowledge_synthesizer.md не содержит 'confidence_rated_conclusions'" - ) - - def test_prompt_contains_phases_context_used(self): - """Промпт содержит поле phases_context_used в Return Format.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "phases_context_used" in content, ( - "knowledge_synthesizer.md не содержит 'phases_context_used'" - ) - - def test_prompt_contains_unresolved_conflicts(self): - """Промпт содержит поле unresolved_conflicts в Return Format.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "unresolved_conflicts" in content, ( - "knowledge_synthesizer.md не содержит 'unresolved_conflicts'" - ) - - def test_prompt_contains_blocked_protocol(self): - """Промпт содержит Blocked Protocol с инструкцией blocked_reason.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "blocked_reason" in content, ( - "knowledge_synthesizer.md не содержит 'blocked_reason' — Blocked Protocol обязателен" - ) - - def test_prompt_no_legacy_output_format_header(self): - """Промпт НЕ содержит устаревшей секции '## Output format'.""" - content = (PROMPTS_DIR / "knowledge_synthesizer.md").read_text(encoding="utf-8") - assert "## Output format" not in content, ( - "knowledge_synthesizer.md содержит устаревшую секцию '## Output format'" - ) - - -# =========================================================================== -# 2. Регистрация в specialists.yaml -# =========================================================================== - -class TestKnowledgeSynthesizerSpecialists: - """Тесты регистрации knowledge_synthesizer в agents/specialists.yaml.""" - - def test_role_exists_in_specialists(self): - """specialists.yaml содержит роль knowledge_synthesizer.""" - data = _load_yaml() - assert "knowledge_synthesizer" in data.get("specialists", {}), ( - "knowledge_synthesizer отсутствует в specialists.yaml" - ) - - def test_role_model_is_sonnet(self): - """knowledge_synthesizer использует модель sonnet.""" - data = _load_yaml() - role = data["specialists"]["knowledge_synthesizer"] - assert role.get("model") == "sonnet", ( - f"Ожидался model=sonnet, получили: {role.get('model')}" - ) - - def test_role_tools_include_read_grep_glob(self): - """knowledge_synthesizer имеет инструменты Read, Grep, Glob.""" - data = _load_yaml() - tools = data["specialists"]["knowledge_synthesizer"].get("tools", []) - for required_tool in ("Read", "Grep", "Glob"): - assert required_tool in tools, ( - f"knowledge_synthesizer должен иметь инструмент {required_tool!r}" - ) - - def test_role_has_no_write_tools(self): - """knowledge_synthesizer НЕ имеет write-инструментов (Read-only роль).""" - data = _load_yaml() - tools = set(data["specialists"]["knowledge_synthesizer"].get("tools", [])) - write_tools = {"Write", "Edit", "Bash"} - unexpected = write_tools & tools - assert not unexpected, ( - f"knowledge_synthesizer не должен иметь write-инструменты: {unexpected}" - ) - - def test_role_permissions_is_read_only(self): - """knowledge_synthesizer имеет permissions=read_only.""" - data = _load_yaml() - role = data["specialists"]["knowledge_synthesizer"] - assert role.get("permissions") == "read_only", ( - f"Ожидался permissions=read_only, получили: {role.get('permissions')}" - ) - - def test_role_context_rules_decisions_all(self): - """knowledge_synthesizer получает все decisions (context_rules.decisions=all).""" - data = _load_yaml() - role = data["specialists"]["knowledge_synthesizer"] - decisions = role.get("context_rules", {}).get("decisions") - assert decisions == "all", ( - f"Ожидался context_rules.decisions=all, получили: {decisions}" - ) - - def test_role_has_output_schema(self): - """knowledge_synthesizer имеет поле output_schema.""" - data = _load_yaml() - role = data["specialists"]["knowledge_synthesizer"] - assert "output_schema" in role, ( - "knowledge_synthesizer должен иметь output_schema" - ) - - @pytest.mark.parametrize("field", OUTPUT_SCHEMA_FIELDS) - def test_output_schema_has_required_field(self, field): - """output_schema содержит каждое из 5 обязательных полей.""" - data = _load_yaml() - schema = data["specialists"]["knowledge_synthesizer"]["output_schema"] - assert field in schema, ( - f"output_schema knowledge_synthesizer не содержит обязательного поля {field!r}" - ) - - -# =========================================================================== -# 3. Корректность pipeline с aggregation шагом -# =========================================================================== - -class TestResearchPipelineWithSynthesizer: - """Тесты корректности research pipeline с knowledge_synthesizer aggregation шагом.""" - - def test_build_phase_order_two_researchers_includes_synthesizer(self): - """build_phase_order с 2 исследователями вставляет knowledge_synthesizer перед architect.""" - result = build_phase_order(["business_analyst", "market_researcher"]) - assert "knowledge_synthesizer" in result, ( - "knowledge_synthesizer должен быть в pipeline при ≥2 исследователях" - ) - - def test_build_phase_order_synthesizer_before_architect(self): - """knowledge_synthesizer всегда стоит непосредственно перед architect.""" - result = build_phase_order(["tech_researcher", "ux_designer"]) - idx_syn = result.index("knowledge_synthesizer") - idx_arch = result.index("architect") - assert idx_syn == idx_arch - 1, ( - f"knowledge_synthesizer (pos={idx_syn}) должен идти прямо перед architect (pos={idx_arch})" - ) - - def test_build_phase_order_one_researcher_no_synthesizer(self): - """build_phase_order с 1 исследователем НЕ вставляет knowledge_synthesizer.""" - result = build_phase_order(["business_analyst"]) - assert "knowledge_synthesizer" not in result, ( - "knowledge_synthesizer не должен вставляться при 1 исследователе" - ) - - def test_create_project_two_researchers_excludes_synthesizer_from_phases(self, conn): - """create_project_with_phases НЕ создаёт фазу knowledge_synthesizer — авто-управляемая роль.""" - result = create_project_with_phases( - conn, "p1", "P1", "/p1", - description="Test", selected_roles=["market_researcher", "tech_researcher"], - ) - phase_roles = [ph["role"] for ph in result["phases"]] - assert "knowledge_synthesizer" not in phase_roles, ( - f"knowledge_synthesizer не должен быть в pipeline-фазах. Фазы: {phase_roles}" - ) - - def test_create_project_two_researchers_phase_count(self, conn): - """create_project_with_phases с 2 исследователями создаёт 3 фазы (без synthesizer).""" - result = create_project_with_phases( - conn, "p1", "P1", "/p1", - description="Test", selected_roles=["market_researcher", "tech_researcher"], - ) - assert len(result["phases"]) == 3, ( - f"Ожидалось 3 фазы (market_researcher, tech_researcher, architect), " - f"получили {len(result['phases'])}: {[ph['role'] for ph in result['phases']]}" - ) - - def test_create_project_two_researchers_architect_last(self, conn): - """create_project_with_phases с 2 исследователями ставит architect последним.""" - result = create_project_with_phases( - conn, "p1", "P1", "/p1", - description="Test", selected_roles=["market_researcher", "tech_researcher"], - ) - assert result["phases"][-1]["role"] == "architect" - - -# =========================================================================== -# 4. context_builder — knowledge_synthesizer -# =========================================================================== - -class TestKnowledgeSynthesizerContextBuilder: - """Тесты context_builder для роли knowledge_synthesizer.""" - - def test_knowledge_synthesizer_gets_decisions(self, conn_with_project): - """build_context для knowledge_synthesizer включает decisions.""" - models.add_decision( - conn_with_project, "p1", "gotcha", "Some decision", "Details" - ) - ctx = build_context(conn_with_project, "P1-001", "knowledge_synthesizer", "p1") - assert "decisions" in ctx, ( - "knowledge_synthesizer должен получать decisions в контексте" - ) - - def test_knowledge_synthesizer_does_not_get_modules(self, conn_with_project): - """build_context для knowledge_synthesizer НЕ включает modules (в отличие от architect).""" - models.add_module(conn_with_project, "p1", "api", "backend", "src/api/") - ctx = build_context(conn_with_project, "P1-001", "knowledge_synthesizer", "p1") - assert "modules" not in ctx, ( - "knowledge_synthesizer не должен получать modules — только decisions" - ) - - def test_architect_gets_both_decisions_and_modules(self, conn_with_project): - """Регрессия: architect по-прежнему получает decisions И modules.""" - models.add_module(conn_with_project, "p1", "api", "backend", "src/api/") - models.add_decision( - conn_with_project, "p1", "convention", "Use WAL", "SQLite WAL" - ) - ctx = build_context(conn_with_project, "P1-001", "architect", "p1") - assert "decisions" in ctx - assert "modules" in ctx - - -# =========================================================================== -# 5. activate_phase — phases_context injection -# =========================================================================== - -class TestActivatePhaseKnowledgeSynthesizer: - """Тесты activate_phase для роли knowledge_synthesizer.""" - - def test_activate_phase_synthesizer_no_approved_phases_no_context(self, conn): - """activate_phase для knowledge_synthesizer без одобренных фаз не добавляет phases_context.""" - models.create_project(conn, "p1", "P1", "/p1", description="Test") - phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 0) - result = activate_phase(conn, phase["id"]) - task = models.get_task(conn, result["task_id"]) - brief = task["brief"] - assert "phases_context" not in brief, ( - "phases_context не должен быть в brief, если нет одобренных фаз" - ) - - def test_activate_phase_synthesizer_with_approved_phases_injects_context(self, conn): - """activate_phase для knowledge_synthesizer собирает phases_context из одобренных фаз.""" - models.create_project(conn, "p1", "P1", "/p1", description="Test") - - # Создать и одобрить исследовательскую фазу - researcher_phase = models.create_phase(conn, "p1", "business_analyst", 0) - researcher_phase = activate_phase(conn, researcher_phase["id"]) - researcher_task_id = researcher_phase["task_id"] - - # Записать лог агента с output_summary для задачи исследователя - conn.execute( - """INSERT INTO agent_logs (project_id, task_id, agent_role, action, output_summary, success) - VALUES (?, ?, ?, ?, ?, ?)""", - ("p1", researcher_task_id, "business_analyst", "run", - "Market shows strong demand for product.", 1), - ) - conn.commit() - - # Одобрить исследовательскую фазу - models.update_phase(conn, researcher_phase["id"], status="approved") - - # Создать и активировать фазу knowledge_synthesizer - synth_phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 1) - result = activate_phase(conn, synth_phase["id"]) - task = models.get_task(conn, result["task_id"]) - brief = task["brief"] - - assert "phases_context" in brief, ( - "phases_context должен быть в brief при наличии одобренных фаз с agent_logs" - ) - assert "business_analyst" in brief["phases_context"], ( - "phases_context должен содержать ключ 'business_analyst'" - ) - assert "Market shows strong demand" in brief["phases_context"]["business_analyst"], ( - "phases_context['business_analyst'] должен содержать output_summary из agent_logs" - ) - - def test_activate_phase_synthesizer_ignores_pending_phases(self, conn): - """activate_phase для knowledge_synthesizer не включает в phases_context неодобренные фазы.""" - models.create_project(conn, "p1", "P1", "/p1", description="Test") - - # Создать исследовательскую фазу в статусе active (не approved) - researcher_phase = models.create_phase(conn, "p1", "tech_researcher", 0) - researcher_phase = activate_phase(conn, researcher_phase["id"]) - researcher_task_id = researcher_phase["task_id"] - - # Записать лог агента - conn.execute( - """INSERT INTO agent_logs (project_id, task_id, agent_role, action, output_summary, success) - VALUES (?, ?, ?, ?, ?, ?)""", - ("p1", researcher_task_id, "tech_researcher", "run", "Tech analysis done.", 1), - ) - conn.commit() - # Фаза остаётся в статусе active (не approved) - - # Активировать knowledge_synthesizer - synth_phase = models.create_phase(conn, "p1", "knowledge_synthesizer", 1) - result = activate_phase(conn, synth_phase["id"]) - task = models.get_task(conn, result["task_id"]) - brief = task["brief"] - - # phases_context должен отсутствовать или быть пустым - phases_context = brief.get("phases_context", {}) - assert "tech_researcher" not in phases_context, ( - "phases_context не должен включать фазы без статуса approved" - ) - - -# =========================================================================== -# 6. Exclusion list guard (#921, #917) -# =========================================================================== - -class TestExclusionListRegression: - """Верифицирует, что knowledge_synthesizer.md не добавлен в exclusion list (#921, #917).""" - - def test_knowledge_synthesizer_not_in_exclusion_list(self): - """knowledge_synthesizer.md не включён в EXCLUDED_FROM_STRUCTURE_CHECK.""" - from tests.test_kin_docs_002_regression import EXCLUDED_FROM_STRUCTURE_CHECK - assert "knowledge_synthesizer.md" not in EXCLUDED_FROM_STRUCTURE_CHECK, ( - "knowledge_synthesizer.md не должен быть в EXCLUDED_FROM_STRUCTURE_CHECK — " - "роль должна проходить все стандартные структурные проверки" - ) - - def test_exclusion_list_is_empty(self): - """EXCLUDED_FROM_STRUCTURE_CHECK остаётся пустым — guard от #929.""" - from tests.test_kin_docs_002_regression import EXCLUDED_FROM_STRUCTURE_CHECK - assert EXCLUDED_FROM_STRUCTURE_CHECK == [], ( - f"EXCLUDED_FROM_STRUCTURE_CHECK должен быть пустым, " - f"но содержит: {EXCLUDED_FROM_STRUCTURE_CHECK}" - ) diff --git a/tests/test_phases.py b/tests/test_phases.py index 5557a8f..84ab948 100644 --- a/tests/test_phases.py +++ b/tests/test_phases.py @@ -78,7 +78,6 @@ def test_validate_roles_strips_and_lowercases(): @pytest.mark.parametrize("roles,expected", [ - # 1 исследователь → нет knowledge_synthesizer, architect последний ( ["business_analyst"], ["business_analyst", "architect"], @@ -87,21 +86,17 @@ def test_validate_roles_strips_and_lowercases(): ["tech_researcher"], ["tech_researcher", "architect"], ), - # ≥2 исследователей → knowledge_synthesizer авто-вставляется перед architect (KIN-DOCS-003, 2026-03-19) ( ["marketer", "business_analyst"], - ["business_analyst", "marketer", "knowledge_synthesizer", "architect"], + ["business_analyst", "marketer", "architect"], ), ( ["ux_designer", "market_researcher", "tech_researcher"], - ["market_researcher", "tech_researcher", "ux_designer", "knowledge_synthesizer", "architect"], + ["market_researcher", "tech_researcher", "ux_designer", "architect"], ), ]) def test_build_phase_order_canonical_order_and_appends_architect(roles, expected): - """KIN-059: роли сортируются в канонический порядок, architect добавляется последним. - - При ≥2 исследователях knowledge_synthesizer авто-вставляется перед architect. - """ + """KIN-059: роли сортируются в канонический порядок, architect добавляется последним.""" assert build_phase_order(roles) == expected @@ -118,28 +113,6 @@ def test_build_phase_order_architect_always_last(): assert result[-1] == "architect" -def test_build_phase_order_single_researcher_no_synthesizer(): - """KIN-DOCS-003: 1 исследователь → knowledge_synthesizer НЕ вставляется.""" - result = build_phase_order(["business_analyst"]) - assert "knowledge_synthesizer" not in result - assert result == ["business_analyst", "architect"] - - -def test_build_phase_order_two_researchers_inserts_synthesizer(): - """KIN-DOCS-003: 2 исследователя → knowledge_synthesizer авто-вставляется перед architect.""" - result = build_phase_order(["market_researcher", "tech_researcher"]) - assert "knowledge_synthesizer" in result - assert result.index("knowledge_synthesizer") == len(result) - 2 - assert result[-1] == "architect" - - -def test_validate_roles_strips_knowledge_synthesizer(): - """KIN-DOCS-003: knowledge_synthesizer убирается из входных ролей — авто-управляемая роль.""" - result = validate_roles(["knowledge_synthesizer", "business_analyst"]) - assert "knowledge_synthesizer" not in result - assert "business_analyst" in result - - # --------------------------------------------------------------------------- # create_project_with_phases # --------------------------------------------------------------------------- @@ -173,7 +146,7 @@ def test_create_project_with_phases_other_phases_remain_pending(conn): conn, "proj1", "P1", "/path", description="Desc", selected_roles=["market_researcher", "tech_researcher"], ) - # market_researcher, tech_researcher, architect → 3 фазы; knowledge_synthesizer не фаза (P1-001) + # market_researcher, tech_researcher, architect → 3 фазы for phase in result["phases"][1:]: assert phase["status"] == "pending" @@ -394,89 +367,3 @@ def test_revise_phase_updates_task_id_to_new_task(conn): new_task_id = out["phase"]["task_id"] assert new_task_id != original_task_id assert new_task_id == out["new_task"]["id"] - - -# --------------------------------------------------------------------------- -# Regression: knowledge_synthesizer не создаётся как pipeline-фаза (P1-001) -# --------------------------------------------------------------------------- - - -def test_create_project_with_phases_knowledge_synthesizer_not_in_phases(conn): - """Регрессия P1-001: knowledge_synthesizer не создаётся как pipeline-фаза. - - При ≥2 исследователях build_phase_order включает knowledge_synthesizer, - но create_project_with_phases должен фильтровать его из DB-фаз. - """ - result = create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["market_researcher", "tech_researcher"], - ) - roles = [ph["role"] for ph in result["phases"]] - assert "knowledge_synthesizer" not in roles - - -def test_create_project_with_phases_two_researchers_creates_three_phases(conn): - """Регрессия P1-001: 2 исследователя → 3 фазы (researcher + researcher + architect), не 4. - - До фикса knowledge_synthesizer создавался как фаза → 4 фазы вместо 3. - """ - result = create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["market_researcher", "tech_researcher"], - ) - assert len(result["phases"]) == 3 - roles = [ph["role"] for ph in result["phases"]] - assert roles == ["market_researcher", "tech_researcher", "architect"] - - -# --------------------------------------------------------------------------- -# Edge cases: "effectively empty" arrays — непустой вход, ноль валидных ролей (P1-001) -# --------------------------------------------------------------------------- - - -def test_create_project_with_phases_raises_if_only_architect(conn): - """P1-001 edge case: [architect] → ValueError после фильтрации (architect не researcher).""" - with pytest.raises(ValueError, match="[Aa]t least one research role"): - create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["architect"], - ) - - -def test_create_project_with_phases_raises_if_only_knowledge_synthesizer(conn): - """P1-001 edge case: [knowledge_synthesizer] → ValueError (авто-управляемая роль фильтруется).""" - with pytest.raises(ValueError, match="[Aa]t least one research role"): - create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["knowledge_synthesizer"], - ) - - -def test_create_project_with_phases_raises_if_architect_and_synthesizer_only(conn): - """P1-001 edge case: [architect, knowledge_synthesizer] → ValueError (оба авто-управляемые).""" - with pytest.raises(ValueError, match="[Aa]t least one research role"): - create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["architect", "knowledge_synthesizer"], - ) - - -def test_create_project_with_phases_raises_if_unknown_roles_only(conn): - """P1-001 edge case: только неизвестные роли → пустой список после validate_roles → ValueError.""" - with pytest.raises(ValueError, match="[Aa]t least one research role"): - create_project_with_phases( - conn, "proj1", "P1", "/path", - description="Desc", selected_roles=["wizard", "ghost", "oracle"], - ) - - -def test_build_phase_order_architect_only_returns_empty(): - """P1-001 edge case: build_phase_order([architect]) → [] (architect не researcher).""" - result = build_phase_order(["architect"]) - assert result == [] - - -def test_validate_roles_architect_and_synthesizer_returns_empty(): - """P1-001 edge case: validate_roles([architect, knowledge_synthesizer]) → [] (оба отфильтровываются).""" - result = validate_roles(["architect", "knowledge_synthesizer"]) - assert result == []