devflow-engine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devflow_engine/__init__.py +3 -0
- devflow_engine/agentic_prompts.py +100 -0
- devflow_engine/agentic_runtime.py +398 -0
- devflow_engine/api_key_flow_harness.py +539 -0
- devflow_engine/api_keys.py +357 -0
- devflow_engine/bootstrap/__init__.py +2 -0
- devflow_engine/bootstrap/provision_from_template.py +84 -0
- devflow_engine/cli/__init__.py +0 -0
- devflow_engine/cli/app.py +7270 -0
- devflow_engine/core/__init__.py +0 -0
- devflow_engine/core/config.py +86 -0
- devflow_engine/core/logging.py +29 -0
- devflow_engine/core/paths.py +45 -0
- devflow_engine/core/toml_kv.py +33 -0
- devflow_engine/devflow_event_worker.py +1292 -0
- devflow_engine/devflow_state.py +201 -0
- devflow_engine/devin2/__init__.py +9 -0
- devflow_engine/devin2/agent_definition.py +120 -0
- devflow_engine/devin2/pi_runner.py +204 -0
- devflow_engine/devin_orchestration.py +69 -0
- devflow_engine/docs/prompts/anti-patterns.md +42 -0
- devflow_engine/docs/prompts/devin-agent-prompt.md +55 -0
- devflow_engine/docs/prompts/devin2-agent-prompt.md +81 -0
- devflow_engine/docs/prompts/examples/devin-vapi-clone-reference-exchange.json +85 -0
- devflow_engine/doctor/__init__.py +2 -0
- devflow_engine/doctor/triage.py +140 -0
- devflow_engine/error/__init__.py +0 -0
- devflow_engine/error/remediation.py +21 -0
- devflow_engine/errors/error_solver_dag.py +522 -0
- devflow_engine/errors/runtime_observability.py +67 -0
- devflow_engine/idea/__init__.py +4 -0
- devflow_engine/idea/actors.py +481 -0
- devflow_engine/idea/agentic.py +465 -0
- devflow_engine/idea/analyze.py +93 -0
- devflow_engine/idea/devin_chat_dag.py +1 -0
- devflow_engine/idea/diff.py +99 -0
- devflow_engine/idea/drafts.py +446 -0
- devflow_engine/idea/idea_creation_dag.py +643 -0
- devflow_engine/idea/ideation_enrichment.py +355 -0
- devflow_engine/idea/ideation_enrichment_worker.py +19 -0
- devflow_engine/idea/paths.py +28 -0
- devflow_engine/idea/promote.py +53 -0
- devflow_engine/idea/redaction.py +27 -0
- devflow_engine/idea/repo_tools.py +1277 -0
- devflow_engine/idea/response_mode.py +30 -0
- devflow_engine/idea/story_pipeline.py +1585 -0
- devflow_engine/idea/sufficiency.py +376 -0
- devflow_engine/idea/traditional_stories.py +1257 -0
- devflow_engine/implementation/__init__.py +0 -0
- devflow_engine/implementation/alembic_preflight.py +700 -0
- devflow_engine/implementation/dag.py +8450 -0
- devflow_engine/implementation/green_gate.py +93 -0
- devflow_engine/implementation/prompts.py +108 -0
- devflow_engine/implementation/test_runtime.py +623 -0
- devflow_engine/integration/__init__.py +19 -0
- devflow_engine/integration/agentic.py +66 -0
- devflow_engine/integration/dag.py +3539 -0
- devflow_engine/integration/prompts.py +114 -0
- devflow_engine/integration/supabase_schema.sql +31 -0
- devflow_engine/integration/supabase_sync.py +177 -0
- devflow_engine/llm/__init__.py +1 -0
- devflow_engine/llm/cli_one_shot.py +84 -0
- devflow_engine/llm/cli_stream.py +371 -0
- devflow_engine/llm/execution_context.py +26 -0
- devflow_engine/llm/invoke.py +1322 -0
- devflow_engine/llm/provider_api.py +304 -0
- devflow_engine/llm/repo_knowledge.py +588 -0
- devflow_engine/llm_primitives.py +315 -0
- devflow_engine/orchestration.py +62 -0
- devflow_engine/planning/__init__.py +0 -0
- devflow_engine/planning/analyze_repo.py +92 -0
- devflow_engine/planning/render_drafts.py +133 -0
- devflow_engine/playground/__init__.py +0 -0
- devflow_engine/playground/hooks.py +26 -0
- devflow_engine/playwright_workflow/__init__.py +5 -0
- devflow_engine/playwright_workflow/dag.py +1317 -0
- devflow_engine/process/__init__.py +5 -0
- devflow_engine/process/dag.py +59 -0
- devflow_engine/project_registration/__init__.py +3 -0
- devflow_engine/project_registration/dag.py +1581 -0
- devflow_engine/project_registry.py +109 -0
- devflow_engine/prompts/devin/generic/prompt.md +6 -0
- devflow_engine/prompts/devin/ideation/prompt.md +263 -0
- devflow_engine/prompts/devin/ideation/scenarios.md +5 -0
- devflow_engine/prompts/devin/ideation_loop/prompt.md +6 -0
- devflow_engine/prompts/devin/insight/prompt.md +11 -0
- devflow_engine/prompts/devin/insight/scenarios.md +5 -0
- devflow_engine/prompts/devin/intake/prompt.md +15 -0
- devflow_engine/prompts/devin/iterate/prompt.md +12 -0
- devflow_engine/prompts/devin/shared/eval_doctrine.md +9 -0
- devflow_engine/prompts/devin/shared/principles.md +246 -0
- devflow_engine/prompts/devin_eval/assessment/prompt.md +18 -0
- devflow_engine/prompts/idea/api_ideation_agent/prompt.md +8 -0
- devflow_engine/prompts/idea/api_insight_agent/prompt.md +8 -0
- devflow_engine/prompts/idea/response_doctrine/prompt.md +18 -0
- devflow_engine/prompts/implementation/dependency_assessment/prompt.md +12 -0
- devflow_engine/prompts/implementation/green/green/prompt.md +11 -0
- devflow_engine/prompts/implementation/green/node_config/prompt.md +3 -0
- devflow_engine/prompts/implementation/green_review/outcome_review/prompt.md +5 -0
- devflow_engine/prompts/implementation/green_review/prior_run_review/prompt.md +5 -0
- devflow_engine/prompts/implementation/red/prompt.md +27 -0
- devflow_engine/prompts/implementation/redreview/prompt.md +23 -0
- devflow_engine/prompts/implementation/redreview_repair/prompt.md +16 -0
- devflow_engine/prompts/implementation/setupdoc/prompt.md +10 -0
- devflow_engine/prompts/implementation/story_planning/prompt.md +13 -0
- devflow_engine/prompts/implementation/test_design/prompt.md +27 -0
- devflow_engine/prompts/integration/README.md +185 -0
- devflow_engine/prompts/integration/green/example.md +67 -0
- devflow_engine/prompts/integration/green/green/prompt.md +10 -0
- devflow_engine/prompts/integration/green/node_config/prompt.md +42 -0
- devflow_engine/prompts/integration/green/past_prompts/20260417T212300/green/prompt.md +15 -0
- devflow_engine/prompts/integration/green/past_prompts/20260417T212300/node_config/prompt.md +42 -0
- devflow_engine/prompts/integration/green_enrich/example.md +79 -0
- devflow_engine/prompts/integration/green_enrich/green_enrich/prompt.md +9 -0
- devflow_engine/prompts/integration/green_enrich/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/green_enrich/prompt.md +14 -0
- devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red/code_repair/prompt.md +12 -0
- devflow_engine/prompts/integration/red/example.md +152 -0
- devflow_engine/prompts/integration/red/node_config/prompt.md +86 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/node_config/prompt.md +84 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red/prompt.md +16 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red_repair/prompt.md +15 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/node_config/prompt.md +84 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/red_repair/prompt.md +11 -0
- devflow_engine/prompts/integration/red/red/prompt.md +11 -0
- devflow_engine/prompts/integration/red/red_repair/prompt.md +12 -0
- devflow_engine/prompts/integration/red_review/example.md +71 -0
- devflow_engine/prompts/integration/red_review/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/red_review/prompt.md +15 -0
- devflow_engine/prompts/integration/red_review/red_review/prompt.md +9 -0
- devflow_engine/prompts/integration/resolve/example.md +111 -0
- devflow_engine/prompts/integration/resolve/node_config/prompt.md +64 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/node_config/prompt.md +64 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_implicated_users/prompt.md +15 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_side_effects/prompt.md +15 -0
- devflow_engine/prompts/integration/resolve/resolve_implicated_users/prompt.md +10 -0
- devflow_engine/prompts/integration/resolve/resolve_side_effects/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/build_idea_acceptance_coverage/prompt.md +12 -0
- devflow_engine/prompts/integration/validate/code_repair/prompt.md +13 -0
- devflow_engine/prompts/integration/validate/example.md +143 -0
- devflow_engine/prompts/integration/validate/node_config/prompt.md +87 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/node_config/prompt.md +67 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_enrich_gate/prompt.md +17 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_repair/prompt.md +16 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/node_config/prompt.md +67 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/validate_repair/prompt.md +9 -0
- devflow_engine/prompts/integration/validate/validate_enrich_gate/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/validate_repair/prompt.md +20 -0
- devflow_engine/prompts/integration/write_workflows/example.md +100 -0
- devflow_engine/prompts/integration/write_workflows/node_config/prompt.md +44 -0
- devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/node_config/prompt.md +44 -0
- devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/write_workflows/prompt.md +17 -0
- devflow_engine/prompts/integration/write_workflows/write_workflows/prompt.md +11 -0
- devflow_engine/prompts/iterate/README.md +7 -0
- devflow_engine/prompts/iterate/coder/prompt.md +11 -0
- devflow_engine/prompts/iterate/framer/prompt.md +11 -0
- devflow_engine/prompts/iterate/iterator/prompt.md +13 -0
- devflow_engine/prompts/iterate/observer/prompt.md +11 -0
- devflow_engine/prompts/recovery/diagnosis/prompt.md +7 -0
- devflow_engine/prompts/recovery/execution/prompt.md +8 -0
- devflow_engine/prompts/recovery/execution_verification/prompt.md +7 -0
- devflow_engine/prompts/recovery/failure_investigation/prompt.md +10 -0
- devflow_engine/prompts/recovery/preflight_health_repo_repair/prompt.md +8 -0
- devflow_engine/prompts/recovery/remediation_execution/prompt.md +11 -0
- devflow_engine/prompts/recovery/root_cause_investigation/prompt.md +12 -0
- devflow_engine/prompts/scope_idea/doctrine/prompt.md +7 -0
- devflow_engine/prompts/source_doc_eval/document/prompt.md +6 -0
- devflow_engine/prompts/source_doc_eval/targeted_mutation/prompt.md +9 -0
- devflow_engine/prompts/source_doc_mutation/domain_entities/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/product_brief/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/project_doc_coherence/prompt.md +7 -0
- devflow_engine/prompts/source_doc_mutation/project_doc_render/prompt.md +9 -0
- devflow_engine/prompts/source_doc_mutation/source_doc_coherence/prompt.md +5 -0
- devflow_engine/prompts/source_doc_mutation/source_doc_enrichment_coherence/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/user_workflows/prompt.md +6 -0
- devflow_engine/prompts/source_scope/doctrine/prompt.md +10 -0
- devflow_engine/prompts/ui_grounding/doctrine/prompt.md +7 -0
- devflow_engine/recovery/__init__.py +3 -0
- devflow_engine/recovery/dag.py +2609 -0
- devflow_engine/recovery/models.py +220 -0
- devflow_engine/refactor.py +93 -0
- devflow_engine/registry/__init__.py +1 -0
- devflow_engine/registry/cards.py +238 -0
- devflow_engine/registry/domain_normalize.py +60 -0
- devflow_engine/registry/effects.py +65 -0
- devflow_engine/registry/enforce_report.py +150 -0
- devflow_engine/registry/module_cards_classify.py +164 -0
- devflow_engine/registry/module_cards_draft.py +184 -0
- devflow_engine/registry/module_cards_gate.py +59 -0
- devflow_engine/registry/packages.py +347 -0
- devflow_engine/registry/pathways.py +323 -0
- devflow_engine/review/__init__.py +11 -0
- devflow_engine/review/dag.py +588 -0
- devflow_engine/review/review_story.py +67 -0
- devflow_engine/scope_idea/__init__.py +3 -0
- devflow_engine/scope_idea/agentic.py +39 -0
- devflow_engine/scope_idea/dag.py +1069 -0
- devflow_engine/scope_idea/models.py +175 -0
- devflow_engine/skills/builtins/devflow/queue_failure_investigation/SKILL.md +112 -0
- devflow_engine/skills/builtins/devflow/queue_idea_to_story/SKILL.md +120 -0
- devflow_engine/skills/builtins/devflow/queue_integration/SKILL.md +105 -0
- devflow_engine/skills/builtins/devflow/queue_recovery/SKILL.md +108 -0
- devflow_engine/skills/builtins/devflow/queue_runtime_core/SKILL.md +155 -0
- devflow_engine/skills/builtins/devflow/queue_story_implementation/SKILL.md +122 -0
- devflow_engine/skills/builtins/devin/idea_to_story_handoff/SKILL.md +120 -0
- devflow_engine/skills/builtins/devin/ideation/SKILL.md +168 -0
- devflow_engine/skills/builtins/devin/ideation/state-and-phrasing-reference.md +18 -0
- devflow_engine/skills/builtins/devin/insight/SKILL.md +22 -0
- devflow_engine/skills/registry.example.yaml +42 -0
- devflow_engine/source_doc_assumptions.py +291 -0
- devflow_engine/source_doc_mutation_dag.py +1606 -0
- devflow_engine/source_doc_mutation_eval.py +417 -0
- devflow_engine/source_doc_mutation_worker.py +25 -0
- devflow_engine/source_docs_schema.py +207 -0
- devflow_engine/source_docs_updater.py +309 -0
- devflow_engine/source_scope/__init__.py +15 -0
- devflow_engine/source_scope/agentic.py +45 -0
- devflow_engine/source_scope/dag.py +1626 -0
- devflow_engine/source_scope/models.py +177 -0
- devflow_engine/stores/__init__.py +0 -0
- devflow_engine/stores/execution_store.py +3534 -0
- devflow_engine/story/__init__.py +0 -0
- devflow_engine/story/contracts.py +160 -0
- devflow_engine/story/discovery.py +47 -0
- devflow_engine/story/evidence.py +118 -0
- devflow_engine/story/hashing.py +27 -0
- devflow_engine/story/implemented_queue_purge.py +148 -0
- devflow_engine/story/indexer.py +105 -0
- devflow_engine/story/io.py +20 -0
- devflow_engine/story/markdown_contracts.py +298 -0
- devflow_engine/story/reconciliation.py +408 -0
- devflow_engine/story/validate_stories.py +149 -0
- devflow_engine/story/validate_tests_story.py +512 -0
- devflow_engine/story/validation.py +133 -0
- devflow_engine/ui_grounding/__init__.py +11 -0
- devflow_engine/ui_grounding/agentic.py +31 -0
- devflow_engine/ui_grounding/dag.py +874 -0
- devflow_engine/ui_grounding/models.py +224 -0
- devflow_engine/ui_grounding/pencil_bridge.py +247 -0
- devflow_engine/vendor/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/__init__.py +11 -0
- devflow_engine/vendor/datalumina_genai/core/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/core/exceptions.py +9 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/agent.py +48 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/agent_streaming_node.py +26 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/base.py +89 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/concurrent.py +30 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/router.py +69 -0
- devflow_engine/vendor/datalumina_genai/core/schema.py +72 -0
- devflow_engine/vendor/datalumina_genai/core/task.py +52 -0
- devflow_engine/vendor/datalumina_genai/core/validate.py +139 -0
- devflow_engine/vendor/datalumina_genai/core/workflow.py +200 -0
- devflow_engine/worker.py +1086 -0
- devflow_engine/worker_guard.py +233 -0
- devflow_engine-1.0.0.dist-info/METADATA +235 -0
- devflow_engine-1.0.0.dist-info/RECORD +393 -0
- devflow_engine-1.0.0.dist-info/WHEEL +4 -0
- devflow_engine-1.0.0.dist-info/entry_points.txt +3 -0
- devin/__init__.py +6 -0
- devin/dag.py +58 -0
- devin/dag_two_arm.py +138 -0
- devin/devin_chat_scenario_catalog.json +588 -0
- devin/devin_eval.py +677 -0
- devin/nodes/__init__.py +0 -0
- devin/nodes/ideation/__init__.py +0 -0
- devin/nodes/ideation/node.py +195 -0
- devin/nodes/ideation/playground.py +267 -0
- devin/nodes/ideation/prompt.md +65 -0
- devin/nodes/ideation/scenarios/continue_refinement.py +13 -0
- devin/nodes/ideation/scenarios/continue_refinement_evals.py +18 -0
- devin/nodes/ideation/scenarios/idea_fits_existing_patterns.py +17 -0
- devin/nodes/ideation/scenarios/idea_fits_existing_patterns_evals.py +16 -0
- devin/nodes/ideation/scenarios/large_idea_split.py +4 -0
- devin/nodes/ideation/scenarios/large_idea_split_evals.py +17 -0
- devin/nodes/ideation/scenarios/source_documentation_added.py +4 -0
- devin/nodes/ideation/scenarios/source_documentation_added_evals.py +16 -0
- devin/nodes/ideation/scenarios/user_says_create_it.py +30 -0
- devin/nodes/ideation/scenarios/user_says_create_it_evals.py +23 -0
- devin/nodes/ideation/scenarios/vague_idea.py +16 -0
- devin/nodes/ideation/scenarios/vague_idea_evals.py +47 -0
- devin/nodes/ideation/tools.json +312 -0
- devin/nodes/insight/__init__.py +0 -0
- devin/nodes/insight/node.py +49 -0
- devin/nodes/insight/playground.py +154 -0
- devin/nodes/insight/prompt.md +61 -0
- devin/nodes/insight/scenarios/architecture_pattern_query.py +15 -0
- devin/nodes/insight/scenarios/architecture_pattern_query_evals.py +25 -0
- devin/nodes/insight/scenarios/codebase_exploration.py +15 -0
- devin/nodes/insight/scenarios/codebase_exploration_evals.py +23 -0
- devin/nodes/insight/scenarios/devin_ideation_routing.py +19 -0
- devin/nodes/insight/scenarios/devin_ideation_routing_evals.py +39 -0
- devin/nodes/insight/scenarios/devin_insight_routing.py +20 -0
- devin/nodes/insight/scenarios/devin_insight_routing_evals.py +40 -0
- devin/nodes/insight/scenarios/operational_debugging.py +15 -0
- devin/nodes/insight/scenarios/operational_debugging_evals.py +23 -0
- devin/nodes/insight/scenarios/operational_question.py +9 -0
- devin/nodes/insight/scenarios/operational_question_evals.py +8 -0
- devin/nodes/insight/scenarios/queue_status.py +15 -0
- devin/nodes/insight/scenarios/queue_status_evals.py +23 -0
- devin/nodes/insight/scenarios/source_doc_explanation.py +14 -0
- devin/nodes/insight/scenarios/source_doc_explanation_evals.py +21 -0
- devin/nodes/insight/scenarios/worker_state_check.py +15 -0
- devin/nodes/insight/scenarios/worker_state_check_evals.py +22 -0
- devin/nodes/insight/tools.json +126 -0
- devin/nodes/intake/__init__.py +0 -0
- devin/nodes/intake/node.py +27 -0
- devin/nodes/intake/playground.py +47 -0
- devin/nodes/intake/prompt.md +12 -0
- devin/nodes/intake/scenarios/ideation_routing.py +4 -0
- devin/nodes/intake/scenarios/ideation_routing_evals.py +5 -0
- devin/nodes/intake/scenarios/insight_routing.py +4 -0
- devin/nodes/intake/scenarios/insight_routing_evals.py +5 -0
- devin/nodes/iterate/README.md +44 -0
- devin/nodes/iterate/__init__.py +1 -0
- devin/nodes/iterate/_archived_design_stages/01-objectives-requirements.md +112 -0
- devin/nodes/iterate/_archived_design_stages/02-evals.md +131 -0
- devin/nodes/iterate/_archived_design_stages/03-tools-and-boundaries.md +110 -0
- devin/nodes/iterate/_archived_design_stages/04-harness-and-playground.md +32 -0
- devin/nodes/iterate/_archived_design_stages/05-prompt-deferred.md +11 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/03-tools-and-boundaries.md +13 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/01-objectives-requirements.md +25 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/02-evals.md +9 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/04-harness-and-playground.md +13 -0
- devin/nodes/iterate/agent-roles.md +89 -0
- devin/nodes/iterate/agents/README.md +10 -0
- devin/nodes/iterate/artifacts.md +504 -0
- devin/nodes/iterate/contract.md +100 -0
- devin/nodes/iterate/eval-plan.md +74 -0
- devin/nodes/iterate/node.py +100 -0
- devin/nodes/iterate/pipeline/README.md +13 -0
- devin/nodes/iterate/playground-contract.md +76 -0
- devin/nodes/iterate/prompt.md +11 -0
- devin/nodes/iterate/scenarios/README.md +38 -0
- devin/nodes/iterate/scenarios/artifact-and-loop-scenarios.md +101 -0
- devin/nodes/iterate/scenarios/coder_artifact_alignment.py +32 -0
- devin/nodes/iterate/scenarios/coder_artifact_alignment_evals.py +45 -0
- devin/nodes/iterate/scenarios/coder_bounded_fix.py +27 -0
- devin/nodes/iterate/scenarios/coder_bounded_fix_evals.py +45 -0
- devin/nodes/iterate/scenarios/devin_iterate_routing.py +21 -0
- devin/nodes/iterate/scenarios/devin_iterate_routing_evals.py +36 -0
- devin/nodes/iterate/scenarios/framer_scope_boundary.py +25 -0
- devin/nodes/iterate/scenarios/framer_scope_boundary_evals.py +57 -0
- devin/nodes/iterate/scenarios/framer_task_framing.py +25 -0
- devin/nodes/iterate/scenarios/framer_task_framing_evals.py +58 -0
- devin/nodes/iterate/scenarios/iterate_error_fix.py +21 -0
- devin/nodes/iterate/scenarios/iterate_error_fix_evals.py +39 -0
- devin/nodes/iterate/scenarios/iterate_quick_change.py +21 -0
- devin/nodes/iterate/scenarios/iterate_quick_change_evals.py +35 -0
- devin/nodes/iterate/scenarios/iterate_to_idea_promotion.py +23 -0
- devin/nodes/iterate/scenarios/iterate_to_idea_promotion_evals.py +53 -0
- devin/nodes/iterate/scenarios/iterate_to_insight_reroute.py +23 -0
- devin/nodes/iterate/scenarios/iterate_to_insight_reroute_evals.py +53 -0
- devin/nodes/iterate/scenarios/observer_evidence_seam.py +28 -0
- devin/nodes/iterate/scenarios/observer_evidence_seam_evals.py +55 -0
- devin/nodes/iterate/scenarios/observer_repro_creation.py +28 -0
- devin/nodes/iterate/scenarios/observer_repro_creation_evals.py +45 -0
- devin/nodes/iterate/scenarios/routing-matrix.md +45 -0
- devin/nodes/shared/__init__.py +0 -0
- devin/nodes/shared/filemaker_expert.md +80 -0
- devin/nodes/shared/filemaker_expert.py +354 -0
- devin/nodes/shared/filemaker_expert_eval/runner.py +176 -0
- devin/nodes/shared/filemaker_expert_eval/scenarios.json +65 -0
- devin/nodes/shared/goldilocks_advisor_eval/runner.py +214 -0
- devin/nodes/shared/goldilocks_advisor_eval/scenarios.json +58 -0
- devin/nodes/shared/helpers.py +156 -0
- devin/nodes/shared/idea_compliance_advisor_eval/runner.py +252 -0
- devin/nodes/shared/idea_compliance_advisor_eval/scenarios.json +75 -0
- devin/nodes/shared/models.py +44 -0
- devin/nodes/shared/post.py +40 -0
- devin/nodes/shared/router.py +107 -0
- devin/nodes/shared/tools.py +191 -0
- devin/shared/devin-chat-rubric.md +237 -0
- devin/shared/devin-chat-scenario-suite.md +90 -0
- devin/shared/eval_doctrine.md +9 -0
|
@@ -0,0 +1,3539 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from urllib.error import HTTPError
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Callable as _Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.request import Request, urlopen
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from ..idea.paths import get_idea_paths
|
|
20
|
+
from ..vendor.datalumina_genai.core.nodes.agent import AgentConfig, AgentNode
|
|
21
|
+
from ..vendor.datalumina_genai.core.nodes.base import Node
|
|
22
|
+
from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
|
|
23
|
+
from ..vendor.datalumina_genai.core.task import TaskContext
|
|
24
|
+
from ..vendor.datalumina_genai.core.workflow import Workflow
|
|
25
|
+
from ..project_registry import find_project_for_repo_root, resolve_project_entry
|
|
26
|
+
from ..project_registration.dag import _infer_owner_repo, _lookup_supabase_project_uuid
|
|
27
|
+
from ..devflow_state import _is_uuid_like, publish_devflow_state
|
|
28
|
+
from . import agentic as integration_agentic
|
|
29
|
+
from .prompts import load_integration_node_instruction
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
INTEGRATION_RESUME_METADATA = "resume_fingerprint.json"
|
|
34
|
+
_SOURCE_EVIDENCE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
|
|
35
|
+
_SOURCE_ROOT_NAMES = ("app", "src")
|
|
36
|
+
_PROJECT_MANIFESTS = ("pyproject.toml", "package.json", "setup.py", "setup.cfg", "requirements.txt", "Pipfile")
|
|
37
|
+
_IGNORED_EVIDENCE_DIR_NAMES = {".devflow", ".git", "node_modules", "dist", "build", "coverage", "__pycache__", ".venv", "venv"}
|
|
38
|
+
_STORY_CODE_PATH_KEYS = {"file_targets", "implementation_targets", "paths", "path", "file_path", "files", "written_paths", "validator_input_paths", "seams", "preferred_pathways"}
|
|
39
|
+
_PROJECT_CODE_ROOT_METADATA_KEYS = ("implementation_roots", "implementation_root", "source_roots", "source_root", "code_roots", "code_root", "app_roots", "app_root", "backend_root", "frontend_root")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _sha256_json(payload: dict[str, Any] | list[Any]) -> str:
|
|
43
|
+
return hashlib.sha256(
|
|
44
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
45
|
+
).hexdigest()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sha256_file(path: Path) -> str:
|
|
49
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _relative_repo_path(*, repo_root: Path, path: Path) -> str:
|
|
53
|
+
try:
|
|
54
|
+
return str(path.relative_to(repo_root))
|
|
55
|
+
except ValueError:
|
|
56
|
+
return str(path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _story_code_paths(payload: Any) -> list[str]:
|
|
60
|
+
results: list[str] = []
|
|
61
|
+
|
|
62
|
+
def _push(value: Any) -> None:
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
candidate = value.strip()
|
|
65
|
+
if candidate:
|
|
66
|
+
results.append(candidate)
|
|
67
|
+
elif isinstance(value, dict):
|
|
68
|
+
path_value = value.get("path")
|
|
69
|
+
if isinstance(path_value, str) and path_value.strip():
|
|
70
|
+
results.append(path_value.strip())
|
|
71
|
+
elif isinstance(value, list):
|
|
72
|
+
for item in value:
|
|
73
|
+
_push(item)
|
|
74
|
+
|
|
75
|
+
def _walk(node: Any) -> None:
|
|
76
|
+
if isinstance(node, dict):
|
|
77
|
+
for key, value in node.items():
|
|
78
|
+
if str(key or "").strip().lower() in _STORY_CODE_PATH_KEYS:
|
|
79
|
+
_push(value)
|
|
80
|
+
_walk(value)
|
|
81
|
+
elif isinstance(node, list):
|
|
82
|
+
for item in node:
|
|
83
|
+
_walk(item)
|
|
84
|
+
|
|
85
|
+
_walk(payload)
|
|
86
|
+
return results
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _story_code_root(repo_root: Path, raw_path: str) -> Path | None:
|
|
90
|
+
candidate = raw_path.strip()
|
|
91
|
+
if not candidate:
|
|
92
|
+
return None
|
|
93
|
+
path = Path(candidate)
|
|
94
|
+
if path.is_absolute():
|
|
95
|
+
try:
|
|
96
|
+
path = path.relative_to(repo_root)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return None
|
|
99
|
+
parts = path.parts
|
|
100
|
+
if not parts or parts[0].startswith('.') or parts[0] in {'tests', 'ai_docs'}:
|
|
101
|
+
return None
|
|
102
|
+
for index, part in enumerate(parts[:-1]):
|
|
103
|
+
if part in _SOURCE_ROOT_NAMES:
|
|
104
|
+
return repo_root.joinpath(*parts[: index + 1])
|
|
105
|
+
if len(parts) > 1:
|
|
106
|
+
return repo_root.joinpath(*parts[:-1])
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _project_configured_code_roots(repo_root: Path) -> list[Path]:
|
|
111
|
+
project_entry = find_project_for_repo_root(repo_root)
|
|
112
|
+
metadata = (project_entry or {}).get("metadata")
|
|
113
|
+
if not isinstance(metadata, dict):
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
roots: list[Path] = []
|
|
117
|
+
for key in _PROJECT_CODE_ROOT_METADATA_KEYS:
|
|
118
|
+
raw = metadata.get(key)
|
|
119
|
+
values = raw if isinstance(raw, list) else [raw]
|
|
120
|
+
for value in values:
|
|
121
|
+
if not isinstance(value, str) or not value.strip():
|
|
122
|
+
continue
|
|
123
|
+
candidate = Path(value.strip())
|
|
124
|
+
if not candidate.is_absolute():
|
|
125
|
+
candidate = repo_root / candidate
|
|
126
|
+
try:
|
|
127
|
+
resolved = candidate.resolve()
|
|
128
|
+
resolved.relative_to(repo_root.resolve())
|
|
129
|
+
except Exception:
|
|
130
|
+
continue
|
|
131
|
+
if resolved.is_dir():
|
|
132
|
+
roots.append(resolved)
|
|
133
|
+
return sorted(set(roots))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _find_manifest_project_roots(repo_root: Path) -> list[Path]:
|
|
137
|
+
project_roots: list[Path] = []
|
|
138
|
+
candidates = [repo_root]
|
|
139
|
+
candidates.extend(
|
|
140
|
+
path
|
|
141
|
+
for path in repo_root.iterdir()
|
|
142
|
+
if path.is_dir() and path.name not in _IGNORED_EVIDENCE_DIR_NAMES and not path.name.startswith(".")
|
|
143
|
+
)
|
|
144
|
+
for path in candidates:
|
|
145
|
+
if any((path / name).exists() for name in _PROJECT_MANIFESTS):
|
|
146
|
+
project_roots.append(path)
|
|
147
|
+
return sorted(set(project_roots))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _expand_project_code_roots(project_roots: list[Path]) -> list[Path]:
|
|
151
|
+
roots: list[Path] = []
|
|
152
|
+
for project_root in project_roots:
|
|
153
|
+
nested_roots = [project_root / name for name in _SOURCE_ROOT_NAMES if (project_root / name).is_dir()]
|
|
154
|
+
if nested_roots:
|
|
155
|
+
roots.extend(nested_roots)
|
|
156
|
+
continue
|
|
157
|
+
direct_files = [child for child in project_root.iterdir() if child.is_file() and child.suffix in _SOURCE_EVIDENCE_EXTENSIONS]
|
|
158
|
+
if direct_files:
|
|
159
|
+
roots.append(project_root)
|
|
160
|
+
return sorted(set(root for root in roots if root.exists()))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _implementation_code_roots(*, repo_root: Path, implemented_stories: list[dict[str, Any]]) -> list[Path]:
|
|
164
|
+
story_roots = sorted({
|
|
165
|
+
root
|
|
166
|
+
for story in implemented_stories
|
|
167
|
+
for raw_path in _story_code_paths(story)
|
|
168
|
+
for root in [_story_code_root(repo_root, raw_path)]
|
|
169
|
+
if root is not None and root.exists()
|
|
170
|
+
})
|
|
171
|
+
if story_roots:
|
|
172
|
+
return story_roots
|
|
173
|
+
|
|
174
|
+
configured_roots = _project_configured_code_roots(repo_root)
|
|
175
|
+
if configured_roots:
|
|
176
|
+
return configured_roots
|
|
177
|
+
|
|
178
|
+
project_roots = _find_manifest_project_roots(repo_root)
|
|
179
|
+
nested_project_roots = [root for root in project_roots if root != repo_root]
|
|
180
|
+
manifest_roots = _expand_project_code_roots(nested_project_roots or project_roots)
|
|
181
|
+
if manifest_roots:
|
|
182
|
+
return manifest_roots
|
|
183
|
+
|
|
184
|
+
return [path for path in (repo_root / 'app', repo_root / 'src') if path.is_dir()]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _iter_code_evidence_files(*, repo_root: Path, implemented_stories: list[dict[str, Any]]) -> list[Path]:
|
|
188
|
+
files: list[Path] = []
|
|
189
|
+
seen: set[Path] = set()
|
|
190
|
+
for root in _implementation_code_roots(repo_root=repo_root, implemented_stories=implemented_stories):
|
|
191
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
192
|
+
dirnames[:] = [name for name in dirnames if name not in _IGNORED_EVIDENCE_DIR_NAMES and not name.startswith('.')]
|
|
193
|
+
current_root = Path(dirpath)
|
|
194
|
+
for filename in filenames:
|
|
195
|
+
file_path = current_root / filename
|
|
196
|
+
if file_path.suffix not in _SOURCE_EVIDENCE_EXTENSIONS or file_path in seen:
|
|
197
|
+
continue
|
|
198
|
+
files.append(file_path)
|
|
199
|
+
seen.add(file_path)
|
|
200
|
+
return sorted(files)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _integration_snapshot_paths(*, repo_root: Path, idea_id: str) -> list[Path]:
|
|
204
|
+
idea_dir = repo_root / ".devflow" / "ideas" / idea_id
|
|
205
|
+
paths: list[Path] = []
|
|
206
|
+
idea_json_path = idea_dir / "idea.json"
|
|
207
|
+
if idea_json_path.exists():
|
|
208
|
+
paths.append(idea_json_path)
|
|
209
|
+
story_sets_dir = idea_dir / "devflow_story_sets"
|
|
210
|
+
implemented_stories: list[dict[str, Any]] = []
|
|
211
|
+
if story_sets_dir.exists():
|
|
212
|
+
for path in sorted(story_sets_dir.rglob("*.json")):
|
|
213
|
+
if path.name == "manifest.json":
|
|
214
|
+
continue
|
|
215
|
+
paths.append(path)
|
|
216
|
+
try:
|
|
217
|
+
story_data = json.loads(path.read_text(encoding="utf-8"))
|
|
218
|
+
except Exception:
|
|
219
|
+
continue
|
|
220
|
+
if isinstance(story_data, dict):
|
|
221
|
+
implemented_stories.append(story_data)
|
|
222
|
+
paths.extend(_iter_code_evidence_files(repo_root=repo_root, implemented_stories=implemented_stories))
|
|
223
|
+
ai_docs_dir = repo_root / "ai_docs"
|
|
224
|
+
if ai_docs_dir.exists():
|
|
225
|
+
paths.extend(sorted(ai_docs_dir.rglob("*.md")))
|
|
226
|
+
return paths
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _compute_integration_freshness(
|
|
230
|
+
*,
|
|
231
|
+
repo_root: Path,
|
|
232
|
+
idea_id: str,
|
|
233
|
+
payload_body: dict[str, Any],
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
payload_sha256 = _sha256_json(payload_body)
|
|
236
|
+
repo_files: list[dict[str, str]] = []
|
|
237
|
+
for path in _integration_snapshot_paths(repo_root=repo_root, idea_id=idea_id):
|
|
238
|
+
repo_files.append({"path": _relative_repo_path(repo_root=repo_root, path=path), "sha256": _sha256_file(path)})
|
|
239
|
+
repo_snapshot_sha256 = _sha256_json(repo_files)
|
|
240
|
+
fingerprint = _sha256_json(
|
|
241
|
+
{
|
|
242
|
+
"idea_id": idea_id,
|
|
243
|
+
"payload_sha256": payload_sha256,
|
|
244
|
+
"repo_snapshot_sha256": repo_snapshot_sha256,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
return {
|
|
248
|
+
"fingerprint": fingerprint,
|
|
249
|
+
"payload_sha256": payload_sha256,
|
|
250
|
+
"repo_snapshot_sha256": repo_snapshot_sha256,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _payload_body_without_freshness(payload: dict[str, Any]) -> dict[str, Any]:
|
|
255
|
+
body = dict(payload)
|
|
256
|
+
body.pop("freshness", None)
|
|
257
|
+
return body
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _load_resume_metadata(run_dir: Path) -> dict[str, Any] | None:
|
|
261
|
+
path = run_dir / INTEGRATION_RESUME_METADATA
|
|
262
|
+
if not path.exists():
|
|
263
|
+
return None
|
|
264
|
+
try:
|
|
265
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
return data if isinstance(data, dict) else None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _resume_dir_matches_fingerprint(*, run_dir: Path, expected_fingerprint: str | None) -> bool:
|
|
272
|
+
if not expected_fingerprint:
|
|
273
|
+
return False
|
|
274
|
+
metadata = _load_resume_metadata(run_dir)
|
|
275
|
+
if metadata is None:
|
|
276
|
+
return False
|
|
277
|
+
return str(metadata.get("fingerprint") or "") == expected_fingerprint
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _write_resume_metadata(run_dir: Path, freshness: dict[str, Any]) -> Path:
|
|
281
|
+
path = run_dir / INTEGRATION_RESUME_METADATA
|
|
282
|
+
_write_json(path, freshness)
|
|
283
|
+
return path
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def prepare_integration_payload(*, repo_root: Path, idea_id: str) -> Path:
|
|
287
|
+
"""Build and write integration_payload.json for the given idea.
|
|
288
|
+
|
|
289
|
+
Scans idea.json, devflow_story_sets, app/src Python files, and ai_docs markdown.
|
|
290
|
+
Returns the path to the written payload file.
|
|
291
|
+
"""
|
|
292
|
+
idea_dir = repo_root / '.devflow' / 'ideas' / idea_id
|
|
293
|
+
idea_json_path = idea_dir / 'idea.json'
|
|
294
|
+
if not idea_json_path.exists():
|
|
295
|
+
raise ValueError(f'idea.json not found: {idea_json_path}')
|
|
296
|
+
implemented_idea = json.loads(idea_json_path.read_text(encoding='utf-8'))
|
|
297
|
+
|
|
298
|
+
story_sets_dir = idea_dir / 'devflow_story_sets'
|
|
299
|
+
implemented_stories: list[dict[str, Any]] = []
|
|
300
|
+
if story_sets_dir.exists():
|
|
301
|
+
for story_path in sorted(story_sets_dir.rglob('*.json')):
|
|
302
|
+
if story_path.name == 'manifest.json':
|
|
303
|
+
continue
|
|
304
|
+
try:
|
|
305
|
+
story_data = json.loads(story_path.read_text(encoding='utf-8'))
|
|
306
|
+
story_id = str(story_data.get('story_id') or story_data.get('id') or story_path.stem)
|
|
307
|
+
title = str(story_data.get('title') or story_data.get('name') or story_id)
|
|
308
|
+
summary = {
|
|
309
|
+
'story_id': story_id,
|
|
310
|
+
'title': title,
|
|
311
|
+
'side_effect_ids': list(story_data.get('side_effect_ids') or []),
|
|
312
|
+
'required_planes': list(story_data.get('required_planes') or []),
|
|
313
|
+
}
|
|
314
|
+
for key in _STORY_CODE_PATH_KEYS:
|
|
315
|
+
if key in story_data:
|
|
316
|
+
summary[key] = story_data.get(key)
|
|
317
|
+
implemented_stories.append(summary)
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
code_evidence: list[dict[str, Any]] = []
|
|
322
|
+
for code_path in _iter_code_evidence_files(repo_root=repo_root, implemented_stories=implemented_stories):
|
|
323
|
+
code_evidence.append({'path': _relative_repo_path(repo_root=repo_root, path=code_path), 'side_effect_ids': [], 'interaction_points': []})
|
|
324
|
+
|
|
325
|
+
source_docs: list[dict[str, Any]] = []
|
|
326
|
+
ai_docs_dir = repo_root / 'ai_docs'
|
|
327
|
+
if ai_docs_dir.exists():
|
|
328
|
+
for md_path in sorted(ai_docs_dir.rglob('*.md')):
|
|
329
|
+
try:
|
|
330
|
+
rel = str(md_path.relative_to(repo_root))
|
|
331
|
+
except ValueError:
|
|
332
|
+
rel = str(md_path)
|
|
333
|
+
source_docs.append({'path': rel})
|
|
334
|
+
|
|
335
|
+
payload: dict[str, Any] = {
|
|
336
|
+
'idea_id': idea_id,
|
|
337
|
+
'implemented_idea': implemented_idea,
|
|
338
|
+
'implemented_stories': implemented_stories,
|
|
339
|
+
'code_evidence': code_evidence,
|
|
340
|
+
'source_docs': source_docs,
|
|
341
|
+
}
|
|
342
|
+
payload['freshness'] = _compute_integration_freshness(
|
|
343
|
+
repo_root=repo_root,
|
|
344
|
+
idea_id=idea_id,
|
|
345
|
+
payload_body=payload,
|
|
346
|
+
)
|
|
347
|
+
payload_path = idea_dir / 'integration_payload.json'
|
|
348
|
+
payload_path.parent.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
payload_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
|
350
|
+
return payload_path
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _keychain_get(service: str, account: str) -> str | None:
|
|
354
|
+
try:
|
|
355
|
+
proc = subprocess.run(
|
|
356
|
+
["security", "find-generic-password", "-s", service, "-a", account, "-w"],
|
|
357
|
+
capture_output=True,
|
|
358
|
+
text=True,
|
|
359
|
+
check=False,
|
|
360
|
+
timeout=10,
|
|
361
|
+
)
|
|
362
|
+
except Exception:
|
|
363
|
+
return None
|
|
364
|
+
if proc.returncode != 0:
|
|
365
|
+
return None
|
|
366
|
+
return proc.stdout.strip() or None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _resolve_supabase_rest_config() -> tuple[str, str] | None:
|
|
370
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
371
|
+
return None
|
|
372
|
+
url = (
|
|
373
|
+
os.environ.get("DEVFLOW_SUPABASE_URL")
|
|
374
|
+
or os.environ.get("SUPABASE_URL")
|
|
375
|
+
or _keychain_get("Supabase URL", "Clarity")
|
|
376
|
+
)
|
|
377
|
+
key = (
|
|
378
|
+
os.environ.get("DEVFLOW_SUPABASE_SERVICE_KEY")
|
|
379
|
+
or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
|
380
|
+
or os.environ.get("SUPABASE_SERVICE_KEY")
|
|
381
|
+
or _keychain_get("Supabase Service Key", "Clarity")
|
|
382
|
+
)
|
|
383
|
+
if not url or not key:
|
|
384
|
+
return None
|
|
385
|
+
return url.rstrip("/"), key
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _postgrest_request(
|
|
389
|
+
*,
|
|
390
|
+
method: str,
|
|
391
|
+
url: str,
|
|
392
|
+
key: str,
|
|
393
|
+
body: Any | None = None,
|
|
394
|
+
prefer: str | None = None,
|
|
395
|
+
) -> Any:
|
|
396
|
+
payload = None if body is None else json.dumps(body).encode("utf-8")
|
|
397
|
+
req = Request(url, data=payload, method=method)
|
|
398
|
+
req.add_header("apikey", key)
|
|
399
|
+
req.add_header("Authorization", f"Bearer {key}")
|
|
400
|
+
if body is not None:
|
|
401
|
+
req.add_header("Content-Type", "application/json")
|
|
402
|
+
if prefer:
|
|
403
|
+
req.add_header("Prefer", prefer)
|
|
404
|
+
with urlopen(req, timeout=30) as resp:
|
|
405
|
+
raw = resp.read().decode("utf-8")
|
|
406
|
+
return json.loads(raw) if raw else None
|
|
407
|
+
|
|
408
|
+
@dataclass(frozen=True)
|
|
409
|
+
class Stage:
|
|
410
|
+
node_id: str
|
|
411
|
+
name: str
|
|
412
|
+
deps: list[str]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@dataclass(frozen=True)
|
|
416
|
+
class IntegrationDagResult:
|
|
417
|
+
exit_code: int
|
|
418
|
+
message: str
|
|
419
|
+
pipeline_dir: Path
|
|
420
|
+
artifacts: dict[str, str]
|
|
421
|
+
iterations_used: int
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
CANONICAL_ACTOR_ALLOWED_COMBINATIONS: set[tuple[str, str, str]] = {
|
|
425
|
+
("human", "open", "public"),
|
|
426
|
+
("human", "organization", "authenticated"),
|
|
427
|
+
("human", "organization", "admin"),
|
|
428
|
+
("human", "global", "super_user"),
|
|
429
|
+
("ai", "organization", "authenticated"),
|
|
430
|
+
("ai", "organization", "admin"),
|
|
431
|
+
("system", "organization", "authenticated"),
|
|
432
|
+
("system", "organization", "admin"),
|
|
433
|
+
("system", "global", "super_user"),
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class CanonicalActor(BaseModel):
|
|
438
|
+
kind: str
|
|
439
|
+
scope: str
|
|
440
|
+
authority: str
|
|
441
|
+
rationale: str = ""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class DeniedActorCandidate(BaseModel):
|
|
445
|
+
kind: str
|
|
446
|
+
scope: str
|
|
447
|
+
authority: str
|
|
448
|
+
reason: str
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class SideEffectArtifactItem(BaseModel):
|
|
452
|
+
id: str
|
|
453
|
+
summary: str
|
|
454
|
+
story_ids: list[str] = Field(default_factory=list)
|
|
455
|
+
rationale: str = ""
|
|
456
|
+
interaction_points: list[dict[str, Any]] = Field(default_factory=list)
|
|
457
|
+
needed_interaction_points: list[dict[str, Any]] = Field(default_factory=list)
|
|
458
|
+
process_sequence: list[str] = Field(default_factory=list)
|
|
459
|
+
branches: list[dict[str, Any]] = Field(default_factory=list)
|
|
460
|
+
resulting_artifacts: list[str] = Field(default_factory=list)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class SideEffectsArtifact(BaseModel):
|
|
464
|
+
idea_id: str
|
|
465
|
+
side_effects: list[SideEffectArtifactItem]
|
|
466
|
+
alignment_context: dict[str, Any] = Field(default_factory=dict)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class ImplicatedUsersArtifact(BaseModel):
|
|
470
|
+
idea_id: str
|
|
471
|
+
implicated_users: list[CanonicalActor]
|
|
472
|
+
denied_candidates: list[DeniedActorCandidate] = Field(default_factory=list)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class StoryBackingItem(BaseModel):
|
|
476
|
+
story_id: str
|
|
477
|
+
title: str
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class WorkflowArtifact(BaseModel):
|
|
481
|
+
workflow_id: str
|
|
482
|
+
idea_id: str
|
|
483
|
+
side_effect: dict[str, str]
|
|
484
|
+
implicated_users: list[dict[str, Any]]
|
|
485
|
+
story_backing: list[dict[str, Any]] = Field(default_factory=list)
|
|
486
|
+
code_backing: list[dict[str, Any]] = Field(default_factory=list)
|
|
487
|
+
source_doc_backing: list[dict[str, Any]] = Field(default_factory=list)
|
|
488
|
+
interaction_points: list[dict[str, Any]] = Field(default_factory=list)
|
|
489
|
+
needed_interaction_points: list[dict[str, Any]] = Field(default_factory=list)
|
|
490
|
+
process_sequence: list[str] = Field(default_factory=list)
|
|
491
|
+
branches: list[dict[str, Any]] = Field(default_factory=list)
|
|
492
|
+
resulting_artifacts: list[str] = Field(default_factory=list)
|
|
493
|
+
mermaid: str | None = None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class WorkflowSetArtifact(BaseModel):
|
|
497
|
+
idea_id: str
|
|
498
|
+
workflows: list[WorkflowArtifact]
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class ValidateEnrichFinding(BaseModel):
|
|
502
|
+
workflow_id: str
|
|
503
|
+
severity: str
|
|
504
|
+
summary: str
|
|
505
|
+
details: list[str] = Field(default_factory=list)
|
|
506
|
+
blocking: bool = False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class ValidateRepairDeltaLedger(BaseModel):
|
|
510
|
+
verdict_changes: list[str] = Field(default_factory=list)
|
|
511
|
+
evidence_changes: list[str] = Field(default_factory=list)
|
|
512
|
+
newly_resolved_seams: list[str] = Field(default_factory=list)
|
|
513
|
+
contradiction_citations: list[str] = Field(default_factory=list)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class IdeaAcceptanceCoverageEntry(BaseModel):
|
|
517
|
+
criterion_index: int
|
|
518
|
+
criterion: str
|
|
519
|
+
verdict: str
|
|
520
|
+
proof_summary: str = ""
|
|
521
|
+
story_ids: list[str] = Field(default_factory=list)
|
|
522
|
+
workflow_ids: list[str] = Field(default_factory=list)
|
|
523
|
+
side_effect_ids: list[str] = Field(default_factory=list)
|
|
524
|
+
evidence_refs: list[str] = Field(default_factory=list)
|
|
525
|
+
gaps: list[str] = Field(default_factory=list)
|
|
526
|
+
missing_seams: list[str] = Field(default_factory=list)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class IdeaAcceptanceCoverageArtifact(BaseModel):
|
|
530
|
+
idea_id: str
|
|
531
|
+
summary: str
|
|
532
|
+
criteria: list[IdeaAcceptanceCoverageEntry] = Field(default_factory=list)
|
|
533
|
+
protected_sections: list[dict[str, Any]] = Field(default_factory=list)
|
|
534
|
+
do_not_touch: list[str] = Field(default_factory=list)
|
|
535
|
+
execution_lineage: dict[str, Any] | None = None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class VegIdeaAcceptanceBuilderArtifact(BaseModel):
|
|
539
|
+
idea_id: str
|
|
540
|
+
summary: str
|
|
541
|
+
enriched_workflows: list[WorkflowArtifact]
|
|
542
|
+
findings: list[ValidateEnrichFinding] = Field(default_factory=list)
|
|
543
|
+
idea_acceptance_coverage: IdeaAcceptanceCoverageArtifact
|
|
544
|
+
repair_delta_ledger: ValidateRepairDeltaLedger | None = None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class HarnessSelection(BaseModel):
|
|
548
|
+
harness_kind: str
|
|
549
|
+
interaction_point_ref: str
|
|
550
|
+
interaction_point_kind: str
|
|
551
|
+
rationale: str
|
|
552
|
+
gap_explicit: bool = False
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class IntegrationAssertion(BaseModel):
|
|
556
|
+
assertion_id: str
|
|
557
|
+
summary: str
|
|
558
|
+
evidence_anchor: str = ""
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class RedWorkflowPackage(BaseModel):
|
|
562
|
+
workflow_id: str
|
|
563
|
+
side_effect_id: str
|
|
564
|
+
status: str
|
|
565
|
+
harness_selection: HarnessSelection
|
|
566
|
+
failing_artifacts: list[dict[str, Any]] = Field(default_factory=list)
|
|
567
|
+
expected_assertions: list[IntegrationAssertion] = Field(default_factory=list)
|
|
568
|
+
implicated_user_routes: list[dict[str, Any]] = Field(default_factory=list)
|
|
569
|
+
gap_notes: list[str] = Field(default_factory=list)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class RedArtifact(BaseModel):
|
|
573
|
+
idea_id: str
|
|
574
|
+
packages: list[RedWorkflowPackage]
|
|
575
|
+
summary: str
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class RedReviewFinding(BaseModel):
|
|
579
|
+
workflow_id: str
|
|
580
|
+
verdict: str
|
|
581
|
+
findings: list[str] = Field(default_factory=list)
|
|
582
|
+
repair_directions: list[str] = Field(default_factory=list)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class RedReviewArtifact(BaseModel):
|
|
586
|
+
idea_id: str
|
|
587
|
+
summary: str
|
|
588
|
+
reviewed_packages: list[RedWorkflowPackage]
|
|
589
|
+
findings: list[RedReviewFinding]
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class GreenWorkflowPackage(BaseModel):
|
|
593
|
+
workflow_id: str
|
|
594
|
+
side_effect_id: str
|
|
595
|
+
implementation_changes: list[dict[str, Any]] = Field(default_factory=list)
|
|
596
|
+
harness_support_changes: list[dict[str, Any]] = Field(default_factory=list)
|
|
597
|
+
passing_strategy: list[str] = Field(default_factory=list)
|
|
598
|
+
proof_artifacts: list[dict[str, Any]] = Field(default_factory=list)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class GreenArtifact(BaseModel):
|
|
602
|
+
idea_id: str
|
|
603
|
+
summary: str
|
|
604
|
+
packages: list[GreenWorkflowPackage]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class GreenEnrichWorkflowPackage(BaseModel):
|
|
608
|
+
workflow_id: str
|
|
609
|
+
strengthened_assertions: list[IntegrationAssertion] = Field(default_factory=list)
|
|
610
|
+
observability_improvements: list[dict[str, Any]] = Field(default_factory=list)
|
|
611
|
+
brittleness_reductions: list[str] = Field(default_factory=list)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class GreenEnrichArtifact(BaseModel):
|
|
615
|
+
idea_id: str
|
|
616
|
+
summary: str
|
|
617
|
+
packages: list[GreenEnrichWorkflowPackage]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class CommitWorkflowPackage(BaseModel):
|
|
621
|
+
workflow_id: str
|
|
622
|
+
side_effect_id: str
|
|
623
|
+
included_artifacts: list[str] = Field(default_factory=list)
|
|
624
|
+
traceability: dict[str, Any] = Field(default_factory=dict)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class CommitArtifact(BaseModel):
|
|
628
|
+
idea_id: str
|
|
629
|
+
summary: str
|
|
630
|
+
packages: list[CommitWorkflowPackage]
|
|
631
|
+
package_manifest: dict[str, Any] = Field(default_factory=dict)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class FilePatch(BaseModel):
|
|
635
|
+
file_path: str # relative to repo_root
|
|
636
|
+
content: str # full file content (overwrite)
|
|
637
|
+
rationale: str = "" # why this patch was needed
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class CodeRepairArtifact(BaseModel):
|
|
641
|
+
patches: list[FilePatch] = Field(default_factory=list)
|
|
642
|
+
updated_workflows: list[WorkflowArtifact] = Field(default_factory=list)
|
|
643
|
+
repair_summary: str = ""
|
|
644
|
+
unresolvable_failures: list[str] = Field(default_factory=list)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _workflow_side_effect_map(side_effects_artifact: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
648
|
+
return {
|
|
649
|
+
str(item.get("id") or ""): dict(item)
|
|
650
|
+
for item in (side_effects_artifact.get("side_effects") or [])
|
|
651
|
+
if str(item.get("id") or "")
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _ground_workflow_in_side_effect(*, workflow: dict[str, Any], side_effects_artifact: dict[str, Any]) -> dict[str, Any]:
|
|
656
|
+
grounded = dict(workflow)
|
|
657
|
+
side_effect_id = str(grounded.get("side_effect", {}).get("id") or "")
|
|
658
|
+
side_effect = _workflow_side_effect_map(side_effects_artifact).get(side_effect_id)
|
|
659
|
+
if side_effect is None:
|
|
660
|
+
return grounded
|
|
661
|
+
grounded["interaction_points"] = list(side_effect.get("interaction_points") or [])
|
|
662
|
+
grounded["needed_interaction_points"] = list(side_effect.get("needed_interaction_points") or [])
|
|
663
|
+
if not grounded.get("process_sequence"):
|
|
664
|
+
grounded["process_sequence"] = list(side_effect.get("process_sequence") or [])
|
|
665
|
+
if not grounded.get("resulting_artifacts"):
|
|
666
|
+
grounded["resulting_artifacts"] = list(side_effect.get("resulting_artifacts") or [])
|
|
667
|
+
return grounded
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _ground_workflows_in_side_effects(*, workflows: list[dict[str, Any]], side_effects_artifact: dict[str, Any]) -> list[dict[str, Any]]:
|
|
671
|
+
return [
|
|
672
|
+
_ground_workflow_in_side_effect(workflow=workflow, side_effects_artifact=side_effects_artifact)
|
|
673
|
+
for workflow in workflows
|
|
674
|
+
]
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _run_veg_idea_acceptance_builder(
|
|
678
|
+
*,
|
|
679
|
+
repo_root: Path,
|
|
680
|
+
pipeline_dir: Path,
|
|
681
|
+
idea_id: str,
|
|
682
|
+
implemented_idea: dict[str, Any] | None,
|
|
683
|
+
implemented_stories: list[dict[str, Any]] | None,
|
|
684
|
+
side_effects_artifact: dict[str, Any],
|
|
685
|
+
implicated_users_artifact: dict[str, Any],
|
|
686
|
+
workflows: list[dict[str, Any]],
|
|
687
|
+
code_evidence: list[dict[str, Any]] | None,
|
|
688
|
+
source_docs: list[dict[str, Any]] | None,
|
|
689
|
+
node_id: str,
|
|
690
|
+
) -> VegIdeaAcceptanceBuilderArtifact:
|
|
691
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
692
|
+
repo_root=repo_root,
|
|
693
|
+
stage_name="build_idea_acceptance_coverage",
|
|
694
|
+
output_model=VegIdeaAcceptanceBuilderArtifact,
|
|
695
|
+
context_payload={
|
|
696
|
+
"idea_id": idea_id,
|
|
697
|
+
"implemented_idea": implemented_idea or {},
|
|
698
|
+
"implemented_stories": list(implemented_stories or []),
|
|
699
|
+
"side_effects_artifact": side_effects_artifact,
|
|
700
|
+
"implicated_users_artifact": implicated_users_artifact,
|
|
701
|
+
"workflows": workflows,
|
|
702
|
+
"code_evidence": list(code_evidence or []),
|
|
703
|
+
"source_docs": list(source_docs or []),
|
|
704
|
+
},
|
|
705
|
+
guidance=[],
|
|
706
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
707
|
+
)
|
|
708
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id=node_id, envelope=envelope)
|
|
709
|
+
return artifact
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def default_integration_stages() -> list[Stage]:
|
|
713
|
+
return [
|
|
714
|
+
Stage("resolve_side_effects", "ResolveSideEffectsAndImplicatedUsers", []),
|
|
715
|
+
Stage("write_workflows", "WriteWorkflowPerSideEffect", ["resolve_side_effects"]),
|
|
716
|
+
Stage("veg", "VEGHybridValidateEnrichGate", ["write_workflows"]),
|
|
717
|
+
Stage("red", "Red", ["veg"]),
|
|
718
|
+
Stage("redreview", "RedReview", ["red"]),
|
|
719
|
+
Stage("green", "Green", ["redreview"]),
|
|
720
|
+
Stage("greenenrich", "GreenEnrich", ["green"]),
|
|
721
|
+
Stage("commit", "Commit", ["greenenrich"]),
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def render_stage_plan(stages: list[Stage]) -> str:
|
|
726
|
+
return "\n".join(
|
|
727
|
+
f"{stage.node_id} ({stage.name}) deps=[{','.join(stage.deps) if stage.deps else '-'}]"
|
|
728
|
+
for stage in stages
|
|
729
|
+
) + "\n"
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _write_json(path: Path, payload: dict[str, Any] | list[Any]) -> None:
|
|
733
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
734
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _slug(value: str) -> str:
|
|
738
|
+
text = re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")
|
|
739
|
+
return text or "item"
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _integration_agent_timeout_seconds() -> int:
|
|
743
|
+
raw = os.environ.get("DEVFLOW_INTEGRATION_AGENT_TIMEOUT")
|
|
744
|
+
if not raw:
|
|
745
|
+
return 1800
|
|
746
|
+
try:
|
|
747
|
+
value = int(raw)
|
|
748
|
+
except ValueError:
|
|
749
|
+
return 1800
|
|
750
|
+
return value if value > 0 else 1800
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _preferred_harness_for_interaction_point(point: dict[str, Any]) -> str:
|
|
754
|
+
kind = str(point.get("kind") or "").strip().lower()
|
|
755
|
+
# Fallback: enriched interaction points may use "type" instead of "kind"
|
|
756
|
+
if not kind:
|
|
757
|
+
kind = _normalize_interaction_type(str(point.get("type") or "").strip().lower())
|
|
758
|
+
if kind == "ui":
|
|
759
|
+
return "playwright"
|
|
760
|
+
if kind == "endpoint":
|
|
761
|
+
return "endpoint_harness"
|
|
762
|
+
if kind == "cli":
|
|
763
|
+
return "cli_harness"
|
|
764
|
+
if kind == "artifact":
|
|
765
|
+
return "artifact_probe"
|
|
766
|
+
if kind in {"background", "event", "queue", "job"}:
|
|
767
|
+
return "background_observer"
|
|
768
|
+
return "integration_harness"
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _normalize_interaction_type(raw_type: str) -> str:
|
|
772
|
+
"""Map enriched interaction-point ``type`` values to canonical ``kind`` values.
|
|
773
|
+
|
|
774
|
+
Enrichment agents generate specific type strings (``api_boundary``,
|
|
775
|
+
``health_check``, …) that don't match the narrow ``kind`` vocabulary used by
|
|
776
|
+
``_preferred_harness_for_interaction_point``. We first try an exact lookup,
|
|
777
|
+
then fall back to keyword-based family matching so newly-invented type
|
|
778
|
+
strings still resolve correctly.
|
|
779
|
+
"""
|
|
780
|
+
_TYPE_TO_KIND: dict[str, str] = {
|
|
781
|
+
"api_endpoint": "endpoint",
|
|
782
|
+
"http_endpoint": "endpoint",
|
|
783
|
+
"rest_endpoint": "endpoint",
|
|
784
|
+
"graphql_endpoint": "endpoint",
|
|
785
|
+
"health_check": "endpoint",
|
|
786
|
+
"webhook": "endpoint",
|
|
787
|
+
"websocket": "endpoint",
|
|
788
|
+
"api_boundary": "endpoint",
|
|
789
|
+
"auth_boundary": "endpoint",
|
|
790
|
+
"network_boundary": "endpoint",
|
|
791
|
+
"cli_command": "cli",
|
|
792
|
+
"cli_entrypoint": "cli",
|
|
793
|
+
"ui_component": "ui",
|
|
794
|
+
"ui_page": "ui",
|
|
795
|
+
"browser": "ui",
|
|
796
|
+
"file_artifact": "artifact",
|
|
797
|
+
"artifact_output": "artifact",
|
|
798
|
+
"background_job": "background",
|
|
799
|
+
"cron_job": "background",
|
|
800
|
+
"queue_consumer": "queue",
|
|
801
|
+
"event_handler": "event",
|
|
802
|
+
}
|
|
803
|
+
exact = _TYPE_TO_KIND.get(raw_type)
|
|
804
|
+
if exact is not None:
|
|
805
|
+
return exact
|
|
806
|
+
# Keyword-family fallback for types not in the exact map
|
|
807
|
+
_FAMILY_KEYWORDS: list[tuple[tuple[str, ...], str]] = [
|
|
808
|
+
(("endpoint", "api", "http", "rest", "graphql", "health", "webhook", "websocket", "boundary"), "endpoint"),
|
|
809
|
+
(("cli",), "cli"),
|
|
810
|
+
(("ui", "browser", "page"), "ui"),
|
|
811
|
+
(("artifact", "file"), "artifact"),
|
|
812
|
+
(("background", "cron", "job"), "background"),
|
|
813
|
+
(("queue",), "queue"),
|
|
814
|
+
(("event",), "event"),
|
|
815
|
+
]
|
|
816
|
+
for keywords, kind in _FAMILY_KEYWORDS:
|
|
817
|
+
if any(kw in raw_type for kw in keywords):
|
|
818
|
+
return kind
|
|
819
|
+
return raw_type
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _load_code_evidence_for_workflows(
|
|
823
|
+
*,
|
|
824
|
+
workflows: list[dict[str, Any]],
|
|
825
|
+
code_evidence: list[dict[str, Any]] | None,
|
|
826
|
+
repo_root: Path,
|
|
827
|
+
max_files: int = 20,
|
|
828
|
+
) -> list[dict[str, Any]]:
|
|
829
|
+
"""Load actual file contents for code evidence relevant to the failing workflows."""
|
|
830
|
+
if not code_evidence:
|
|
831
|
+
return []
|
|
832
|
+
# Collect all side_effect_ids referenced by the failing workflows
|
|
833
|
+
relevant_se_ids: set[str] = set()
|
|
834
|
+
for wf in workflows:
|
|
835
|
+
se = wf.get("side_effect") or {}
|
|
836
|
+
if se.get("id"):
|
|
837
|
+
relevant_se_ids.add(str(se["id"]))
|
|
838
|
+
|
|
839
|
+
loaded: list[dict[str, Any]] = []
|
|
840
|
+
seen: set[str] = set()
|
|
841
|
+
for item in code_evidence:
|
|
842
|
+
if len(loaded) >= max_files:
|
|
843
|
+
break
|
|
844
|
+
item_se_ids = set(str(x) for x in (item.get("side_effect_ids") or []))
|
|
845
|
+
if relevant_se_ids and not relevant_se_ids.intersection(item_se_ids) and item_se_ids:
|
|
846
|
+
continue # not relevant to these workflows
|
|
847
|
+
path_str = str(item.get("path") or "")
|
|
848
|
+
if not path_str or path_str in seen:
|
|
849
|
+
continue
|
|
850
|
+
seen.add(path_str)
|
|
851
|
+
full_path = repo_root / path_str
|
|
852
|
+
if full_path.exists():
|
|
853
|
+
try:
|
|
854
|
+
content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
855
|
+
loaded.append({"path": path_str, "content": content[:8000]}) # cap per file
|
|
856
|
+
except Exception:
|
|
857
|
+
loaded.append({"path": path_str, "content": "(unreadable)"})
|
|
858
|
+
else:
|
|
859
|
+
loaded.append({"path": path_str, "content": "(file not found)"})
|
|
860
|
+
return loaded
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _build_post_integration_playwright_request(
|
|
864
|
+
*,
|
|
865
|
+
idea_id: str,
|
|
866
|
+
trigger_run_id: str | None,
|
|
867
|
+
workflows: list[dict[str, Any]],
|
|
868
|
+
red_artifact: dict[str, Any],
|
|
869
|
+
) -> dict[str, Any]:
|
|
870
|
+
red_packages = {
|
|
871
|
+
str(package.get("workflow_id") or ""): package
|
|
872
|
+
for package in red_artifact.get("packages") or []
|
|
873
|
+
if isinstance(package, dict)
|
|
874
|
+
}
|
|
875
|
+
workflow_requests: list[dict[str, Any]] = []
|
|
876
|
+
for workflow in workflows:
|
|
877
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
878
|
+
package = red_packages.get(workflow_id) or {}
|
|
879
|
+
harness = package.get("harness_selection") or {}
|
|
880
|
+
interaction_points = list(workflow.get("interaction_points") or [])
|
|
881
|
+
ui_points = [
|
|
882
|
+
point for point in interaction_points
|
|
883
|
+
if _preferred_harness_for_interaction_point(point) == "playwright"
|
|
884
|
+
]
|
|
885
|
+
harness_kind = str(harness.get("harness_kind") or "")
|
|
886
|
+
requires_playwright = bool(ui_points or harness_kind == "playwright")
|
|
887
|
+
workflow_requests.append({
|
|
888
|
+
"workflow_id": workflow_id,
|
|
889
|
+
"side_effect_id": str(
|
|
890
|
+
workflow.get("side_effect", {}).get("id")
|
|
891
|
+
or package.get("side_effect_id")
|
|
892
|
+
or ""
|
|
893
|
+
),
|
|
894
|
+
"requires_playwright": requires_playwright,
|
|
895
|
+
"harness_kind": harness_kind or ("playwright" if requires_playwright else "integration_harness"),
|
|
896
|
+
"interaction_points": ui_points if ui_points else interaction_points[:1],
|
|
897
|
+
"story_backing": list(workflow.get("story_backing") or []),
|
|
898
|
+
"required_proof": [
|
|
899
|
+
"real browser execution for visible UI controls",
|
|
900
|
+
"assert observable business results, not only clicks or page load",
|
|
901
|
+
(
|
|
902
|
+
"verify role, status, filter, reset, and cancellation behavior "
|
|
903
|
+
"where the workflow exposes those controls"
|
|
904
|
+
),
|
|
905
|
+
"fail early when expected seed records are missing",
|
|
906
|
+
] if requires_playwright else [],
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
required_workflows = [item for item in workflow_requests if item["requires_playwright"]]
|
|
910
|
+
return {
|
|
911
|
+
"kind": "post_integration_playwright_request.v1",
|
|
912
|
+
"idea_id": idea_id,
|
|
913
|
+
"trigger_run_id": trigger_run_id,
|
|
914
|
+
"status": "required" if required_workflows else "not_required",
|
|
915
|
+
"process": "post_integration_playwright_dag",
|
|
916
|
+
"workflow_requests": workflow_requests,
|
|
917
|
+
"required_workflow_ids": [item["workflow_id"] for item in required_workflows],
|
|
918
|
+
"expected_artifacts": [
|
|
919
|
+
"app_route_inventory.json",
|
|
920
|
+
"ui_element_ledger.json",
|
|
921
|
+
"playwright_coverage_verifier.json",
|
|
922
|
+
"playwright_generation_plan.json",
|
|
923
|
+
"generated_playwright_specs_manifest.json",
|
|
924
|
+
"workflow_preview_manifest.json",
|
|
925
|
+
],
|
|
926
|
+
"blocking_gates": [
|
|
927
|
+
(
|
|
928
|
+
"ui_element_ledger has no uncovered mutation, navigation, destructive, "
|
|
929
|
+
"mode-toggle, filter, sort, search, or form-control rows without a "
|
|
930
|
+
"disabled/deferred rationale"
|
|
931
|
+
),
|
|
932
|
+
"playwright_coverage_verifier.passed is true",
|
|
933
|
+
"generated specs assert business outcomes on the correct proof plane for each workflow",
|
|
934
|
+
(
|
|
935
|
+
"video/preview capture is produced only after passing E2E proof and is "
|
|
936
|
+
"not accepted as a substitute for assertions"
|
|
937
|
+
),
|
|
938
|
+
],
|
|
939
|
+
"worker_handoff": {
|
|
940
|
+
"queue_type": "post_integration_playwright",
|
|
941
|
+
"run_after": "successful integration queue drain with no active recovery",
|
|
942
|
+
"current_artifact_dir": ".devflow/post_integration_playwright/current",
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
class IntegrationDagEvent(BaseModel):
|
|
948
|
+
repo_root: str
|
|
949
|
+
idea_id: str
|
|
950
|
+
implemented_idea: dict[str, Any]
|
|
951
|
+
implemented_stories: list[dict[str, Any]] = Field(default_factory=list)
|
|
952
|
+
code_evidence: list[dict[str, Any]] = Field(default_factory=list)
|
|
953
|
+
source_docs: list[dict[str, Any]] = Field(default_factory=list)
|
|
954
|
+
pipeline_dir: str
|
|
955
|
+
prior_run_dir: str | None = None
|
|
956
|
+
resume_freshness: dict[str, Any] = Field(default_factory=dict)
|
|
957
|
+
project_id: str | None = None
|
|
958
|
+
dag_run_id: str | None = None
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _integration_dfs_progress(task_context: TaskContext, summary: str) -> None:
|
|
962
|
+
"""Publish a progress update to DFS (devflow_state) for the current integration run."""
|
|
963
|
+
project_id = task_context.metadata.get("project_id")
|
|
964
|
+
dag_run_id = task_context.metadata.get("dag_run_id")
|
|
965
|
+
idea_id = getattr(getattr(task_context, "event", None), "idea_id", None)
|
|
966
|
+
if not project_id:
|
|
967
|
+
return
|
|
968
|
+
try:
|
|
969
|
+
publish_devflow_state(
|
|
970
|
+
project_id=project_id,
|
|
971
|
+
run_id=dag_run_id,
|
|
972
|
+
current_state="running",
|
|
973
|
+
current_status="processing",
|
|
974
|
+
run_summary=summary,
|
|
975
|
+
display="project",
|
|
976
|
+
display_path=f"idea:{idea_id}" if idea_id else None,
|
|
977
|
+
)
|
|
978
|
+
except Exception:
|
|
979
|
+
return
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _integration_root(repo_root: Path, idea_id: str) -> Path:
|
|
983
|
+
return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "integration"
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _integration_current_dir(repo_root: Path, idea_id: str) -> Path:
|
|
987
|
+
return _integration_root(repo_root, idea_id) / "current"
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _integration_runs_dir(repo_root: Path, idea_id: str) -> Path:
|
|
991
|
+
return _integration_root(repo_root, idea_id) / "runs"
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _legacy_integration_runs_dir(repo_root: Path, idea_id: str) -> Path:
|
|
995
|
+
return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "pipelines" / "integration_dag"
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _write_integration_artifact(*, repo_root: Path, idea_id: str, pipeline_dir: Path, filename: str, data: dict[str, Any]) -> tuple[Path, Path]:
|
|
999
|
+
run_path = pipeline_dir / filename
|
|
1000
|
+
current_dir = _integration_current_dir(repo_root, idea_id)
|
|
1001
|
+
current_dir.mkdir(parents=True, exist_ok=True)
|
|
1002
|
+
current_path = current_dir / filename
|
|
1003
|
+
_write_json(run_path, data)
|
|
1004
|
+
_write_json(current_path, data)
|
|
1005
|
+
return run_path, current_path
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _invalidate_integration_current_from_stage(*, current_dir: Path, stage: str) -> None:
|
|
1009
|
+
ordered: dict[str, list[str]] = {
|
|
1010
|
+
"resolve": [
|
|
1011
|
+
"side_effects.json",
|
|
1012
|
+
"implicated_users.json",
|
|
1013
|
+
"workflow_inventory.json",
|
|
1014
|
+
"validation_gate.json",
|
|
1015
|
+
"validation_gate_agentic_review.json",
|
|
1016
|
+
"idea_acceptance_coverage.json",
|
|
1017
|
+
"validation_repair_clusters.json",
|
|
1018
|
+
"red_package.json",
|
|
1019
|
+
"red_validation.json",
|
|
1020
|
+
"red_review.json",
|
|
1021
|
+
"green_package.json",
|
|
1022
|
+
"green_enrich.json",
|
|
1023
|
+
"commit_package.json",
|
|
1024
|
+
],
|
|
1025
|
+
"write_workflows": [
|
|
1026
|
+
"workflow_inventory.json",
|
|
1027
|
+
"validation_gate.json",
|
|
1028
|
+
"validation_gate_agentic_review.json",
|
|
1029
|
+
"idea_acceptance_coverage.json",
|
|
1030
|
+
"validation_repair_clusters.json",
|
|
1031
|
+
"red_package.json",
|
|
1032
|
+
"red_validation.json",
|
|
1033
|
+
"red_review.json",
|
|
1034
|
+
"green_package.json",
|
|
1035
|
+
"green_enrich.json",
|
|
1036
|
+
"commit_package.json",
|
|
1037
|
+
],
|
|
1038
|
+
"validate": [
|
|
1039
|
+
"validation_gate.json",
|
|
1040
|
+
"validation_gate_agentic_review.json",
|
|
1041
|
+
"idea_acceptance_coverage.json",
|
|
1042
|
+
"validation_repair_clusters.json",
|
|
1043
|
+
"red_package.json",
|
|
1044
|
+
"red_validation.json",
|
|
1045
|
+
"red_review.json",
|
|
1046
|
+
"green_package.json",
|
|
1047
|
+
"green_enrich.json",
|
|
1048
|
+
"commit_package.json",
|
|
1049
|
+
],
|
|
1050
|
+
"red": [
|
|
1051
|
+
"red_package.json",
|
|
1052
|
+
"red_validation.json",
|
|
1053
|
+
"red_review.json",
|
|
1054
|
+
"green_package.json",
|
|
1055
|
+
"green_enrich.json",
|
|
1056
|
+
"commit_package.json",
|
|
1057
|
+
],
|
|
1058
|
+
"red_review": [
|
|
1059
|
+
"red_review.json",
|
|
1060
|
+
"green_package.json",
|
|
1061
|
+
"green_enrich.json",
|
|
1062
|
+
"commit_package.json",
|
|
1063
|
+
],
|
|
1064
|
+
"green": [
|
|
1065
|
+
"green_package.json",
|
|
1066
|
+
"green_enrich.json",
|
|
1067
|
+
"commit_package.json",
|
|
1068
|
+
],
|
|
1069
|
+
"green_enrich": [
|
|
1070
|
+
"green_enrich.json",
|
|
1071
|
+
"commit_package.json",
|
|
1072
|
+
],
|
|
1073
|
+
"commit": ["commit_package.json"],
|
|
1074
|
+
}
|
|
1075
|
+
targets = ordered.get(stage, [])
|
|
1076
|
+
for name in targets:
|
|
1077
|
+
path = current_dir / name
|
|
1078
|
+
if path.exists():
|
|
1079
|
+
path.unlink()
|
|
1080
|
+
if stage in {"resolve", "write_workflows"}:
|
|
1081
|
+
for path in current_dir.glob("workflow_*.json"):
|
|
1082
|
+
if path.name != "workflow_inventory.json":
|
|
1083
|
+
path.unlink()
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _current_dir_from_task(task_context: TaskContext) -> Path:
|
|
1087
|
+
current_dir = task_context.metadata.get("current_dir")
|
|
1088
|
+
if current_dir:
|
|
1089
|
+
return Path(current_dir)
|
|
1090
|
+
event = task_context.event
|
|
1091
|
+
return _integration_current_dir(Path(event.repo_root), event.idea_id)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _read_resume_artifact(task_context: TaskContext, filename: str) -> tuple[dict[str, Any] | None, Path | None]:
|
|
1095
|
+
event = task_context.event
|
|
1096
|
+
repo_root = Path(event.repo_root)
|
|
1097
|
+
current_dir = _integration_current_dir(repo_root, event.idea_id)
|
|
1098
|
+
current_path = current_dir / filename
|
|
1099
|
+
expected_fingerprint = str(task_context.metadata.get("resume_fingerprint") or "") or None
|
|
1100
|
+
if current_path.exists() and _resume_dir_matches_fingerprint(run_dir=current_dir, expected_fingerprint=expected_fingerprint):
|
|
1101
|
+
try:
|
|
1102
|
+
return json.loads(current_path.read_text(encoding="utf-8")), current_path
|
|
1103
|
+
except Exception:
|
|
1104
|
+
pass
|
|
1105
|
+
prior_str = task_context.metadata.get("prior_run_dir")
|
|
1106
|
+
if prior_str:
|
|
1107
|
+
prior_dir = Path(prior_str)
|
|
1108
|
+
prior_path = prior_dir / filename
|
|
1109
|
+
if prior_path.exists() and _resume_dir_matches_fingerprint(run_dir=prior_dir, expected_fingerprint=expected_fingerprint):
|
|
1110
|
+
try:
|
|
1111
|
+
return json.loads(prior_path.read_text(encoding="utf-8")), prior_path
|
|
1112
|
+
except Exception:
|
|
1113
|
+
pass
|
|
1114
|
+
return None, None
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _try_load_prior_artifact(
|
|
1118
|
+
task_context: TaskContext,
|
|
1119
|
+
*filenames: str,
|
|
1120
|
+
validity_check: _Callable[[dict[str, Any]], bool] | None = None,
|
|
1121
|
+
) -> dict[str, Any] | None:
|
|
1122
|
+
"""Attempt to load a prior run artifact for checkpoint/resume.
|
|
1123
|
+
|
|
1124
|
+
Returns the parsed primary artifact (first filename) when all files exist
|
|
1125
|
+
and the optional validity_check passes; otherwise returns None.
|
|
1126
|
+
"""
|
|
1127
|
+
if task_context.metadata.get("resume_aborted"):
|
|
1128
|
+
return None
|
|
1129
|
+
loaded: list[tuple[dict[str, Any], Path]] = []
|
|
1130
|
+
for fname in filenames:
|
|
1131
|
+
data, path = _read_resume_artifact(task_context, fname)
|
|
1132
|
+
if data is None or path is None:
|
|
1133
|
+
return None
|
|
1134
|
+
loaded.append((data, path))
|
|
1135
|
+
primary = loaded[0][0]
|
|
1136
|
+
if validity_check is not None and not validity_check(primary):
|
|
1137
|
+
return None
|
|
1138
|
+
return primary
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _abort_resume(task_context: TaskContext) -> None:
|
|
1142
|
+
"""Mark resume as aborted so all downstream nodes re-run."""
|
|
1143
|
+
task_context.metadata["resume_aborted"] = True
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def _score_prior_run_dir(run_dir: Path, *, expected_fingerprint: str | None = None) -> tuple[int, float]:
|
|
1147
|
+
"""Prefer the most complete reusable prior integration run, not newest mtime alone."""
|
|
1148
|
+
if not _resume_dir_matches_fingerprint(run_dir=run_dir, expected_fingerprint=expected_fingerprint):
|
|
1149
|
+
return (0, run_dir.stat().st_mtime)
|
|
1150
|
+
score = 0
|
|
1151
|
+
try:
|
|
1152
|
+
inventory_path = run_dir / "workflow_inventory.json"
|
|
1153
|
+
if inventory_path.exists():
|
|
1154
|
+
inventory = json.loads(inventory_path.read_text(encoding="utf-8"))
|
|
1155
|
+
side_effect_ids = [str(sid) for sid in (inventory.get("side_effect_ids") or []) if str(sid)]
|
|
1156
|
+
workflow_files = [run_dir / f"workflow_{_slug(se_id)}.json" for se_id in side_effect_ids]
|
|
1157
|
+
if side_effect_ids and all(path.exists() for path in workflow_files):
|
|
1158
|
+
score = max(score, 1)
|
|
1159
|
+
gate_path = run_dir / "validation_gate.json"
|
|
1160
|
+
if gate_path.exists():
|
|
1161
|
+
gate = json.loads(gate_path.read_text(encoding="utf-8"))
|
|
1162
|
+
if bool(gate.get("passed")) or bool(gate.get("structural_only")):
|
|
1163
|
+
score = max(score, 2)
|
|
1164
|
+
red_validation_path = run_dir / "red_validation.json"
|
|
1165
|
+
if (run_dir / "red_package.json").exists() and red_validation_path.exists():
|
|
1166
|
+
red_validation = json.loads(red_validation_path.read_text(encoding="utf-8"))
|
|
1167
|
+
if bool(red_validation.get("passed")):
|
|
1168
|
+
score = max(score, 3)
|
|
1169
|
+
red_review_path = run_dir / "red_review.json"
|
|
1170
|
+
if red_review_path.exists():
|
|
1171
|
+
red_review = json.loads(red_review_path.read_text(encoding="utf-8"))
|
|
1172
|
+
if bool(red_review.get("agentic_review_clear")):
|
|
1173
|
+
score = max(score, 4)
|
|
1174
|
+
if (run_dir / "green_package.json").exists():
|
|
1175
|
+
score = max(score, 5)
|
|
1176
|
+
if (run_dir / "green_enrich.json").exists():
|
|
1177
|
+
score = max(score, 6)
|
|
1178
|
+
if (run_dir / "commit_package.json").exists():
|
|
1179
|
+
score = max(score, 7)
|
|
1180
|
+
except Exception:
|
|
1181
|
+
score = 0
|
|
1182
|
+
return (score, run_dir.stat().st_mtime)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _select_prior_run_dir(
|
|
1186
|
+
runs_parent: Path,
|
|
1187
|
+
current_pipeline_dir: Path,
|
|
1188
|
+
fallback_runs_parent: Path | None = None,
|
|
1189
|
+
*,
|
|
1190
|
+
expected_fingerprint: str | None = None,
|
|
1191
|
+
) -> Path | None:
|
|
1192
|
+
candidates = [d for d in runs_parent.iterdir() if d.is_dir() and d != current_pipeline_dir] if runs_parent.exists() else []
|
|
1193
|
+
if fallback_runs_parent is not None and fallback_runs_parent.exists():
|
|
1194
|
+
candidates.extend(d for d in fallback_runs_parent.iterdir() if d.is_dir() and d != current_pipeline_dir)
|
|
1195
|
+
if not candidates:
|
|
1196
|
+
return None
|
|
1197
|
+
scored = [
|
|
1198
|
+
(run_dir, *_score_prior_run_dir(run_dir, expected_fingerprint=expected_fingerprint))
|
|
1199
|
+
for run_dir in candidates
|
|
1200
|
+
]
|
|
1201
|
+
usable = [item for item in scored if item[1] > 0]
|
|
1202
|
+
if not usable:
|
|
1203
|
+
return None
|
|
1204
|
+
return max(usable, key=lambda item: (item[1], item[2]))[0]
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
class LoadIntegrationContextNode(Node):
|
|
1208
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1209
|
+
event = task_context.event
|
|
1210
|
+
pipeline_dir = Path(event.pipeline_dir)
|
|
1211
|
+
current_dir = _integration_current_dir(Path(event.repo_root), event.idea_id)
|
|
1212
|
+
current_dir.mkdir(parents=True, exist_ok=True)
|
|
1213
|
+
pipeline_dir.mkdir(parents=True, exist_ok=True)
|
|
1214
|
+
task_context.metadata["pipeline_dir"] = str(pipeline_dir)
|
|
1215
|
+
task_context.metadata["current_dir"] = str(current_dir)
|
|
1216
|
+
task_context.metadata["artifacts"] = {}
|
|
1217
|
+
prior_run_dir = getattr(event, "prior_run_dir", None)
|
|
1218
|
+
task_context.metadata["prior_run_dir"] = prior_run_dir
|
|
1219
|
+
task_context.metadata["resume_aborted"] = False
|
|
1220
|
+
task_context.metadata["project_id"] = getattr(event, "project_id", None)
|
|
1221
|
+
task_context.metadata["dag_run_id"] = getattr(event, "dag_run_id", None)
|
|
1222
|
+
freshness = dict(getattr(event, "resume_freshness", None) or {})
|
|
1223
|
+
task_context.metadata["resume_fingerprint"] = str(freshness.get("fingerprint") or "")
|
|
1224
|
+
if not _resume_dir_matches_fingerprint(
|
|
1225
|
+
run_dir=current_dir,
|
|
1226
|
+
expected_fingerprint=task_context.metadata["resume_fingerprint"],
|
|
1227
|
+
):
|
|
1228
|
+
_invalidate_integration_current_from_stage(current_dir=current_dir, stage="resolve")
|
|
1229
|
+
_write_resume_metadata(pipeline_dir, freshness)
|
|
1230
|
+
_write_resume_metadata(current_dir, freshness)
|
|
1231
|
+
self.save_output({"pipeline_dir": str(pipeline_dir)})
|
|
1232
|
+
return task_context
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class ResolveSideEffectsAndImplicatedUsersNode(AgentNode):
|
|
1236
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1237
|
+
return AgentConfig(
|
|
1238
|
+
instructions=load_integration_node_instruction("resolve"),
|
|
1239
|
+
output_type=SideEffectsArtifact,
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1243
|
+
# -- checkpoint/resume --
|
|
1244
|
+
prior_se = _try_load_prior_artifact(task_context, "side_effects.json", "implicated_users.json")
|
|
1245
|
+
if prior_se is not None:
|
|
1246
|
+
prior_users, prior_users_path = _read_resume_artifact(task_context, "implicated_users.json")
|
|
1247
|
+
side_effects_path = _current_dir_from_task(task_context) / "side_effects.json"
|
|
1248
|
+
implied_path = prior_users_path or (_current_dir_from_task(task_context) / "implicated_users.json")
|
|
1249
|
+
task_context.metadata["side_effects_artifact"] = prior_se
|
|
1250
|
+
task_context.metadata["implicated_users_artifact"] = prior_users or {}
|
|
1251
|
+
task_context.metadata["artifacts"].update({
|
|
1252
|
+
"side_effects": str(side_effects_path),
|
|
1253
|
+
"implicated_users": str(implied_path),
|
|
1254
|
+
})
|
|
1255
|
+
self.save_output({"resumed_from": str(_current_dir_from_task(task_context)), "skipped_llm": True})
|
|
1256
|
+
return task_context
|
|
1257
|
+
_abort_resume(task_context)
|
|
1258
|
+
# -- end checkpoint/resume --
|
|
1259
|
+
|
|
1260
|
+
_integration_dfs_progress(task_context, "Resolving side effects")
|
|
1261
|
+
|
|
1262
|
+
event = task_context.event
|
|
1263
|
+
repo_root = Path(event.repo_root)
|
|
1264
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1265
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="resolve")
|
|
1266
|
+
side_effects_artifact, side_effects_envelope = integration_agentic.run_integration_agent_step(
|
|
1267
|
+
repo_root=repo_root,
|
|
1268
|
+
stage_name="resolve_side_effects",
|
|
1269
|
+
output_model=SideEffectsArtifact,
|
|
1270
|
+
context_payload={
|
|
1271
|
+
"idea_id": event.idea_id,
|
|
1272
|
+
"implemented_idea": event.implemented_idea,
|
|
1273
|
+
"implemented_stories": event.implemented_stories,
|
|
1274
|
+
"code_evidence": event.code_evidence,
|
|
1275
|
+
"source_docs": event.source_docs,
|
|
1276
|
+
"canonical_actor_allowed_combinations": sorted(list(CANONICAL_ACTOR_ALLOWED_COMBINATIONS)),
|
|
1277
|
+
},
|
|
1278
|
+
guidance=[],
|
|
1279
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1280
|
+
)
|
|
1281
|
+
_integration_dfs_progress(task_context, "Resolving users")
|
|
1282
|
+
implicated_users_artifact, users_envelope = integration_agentic.run_integration_agent_step(
|
|
1283
|
+
repo_root=repo_root,
|
|
1284
|
+
stage_name="resolve_implicated_users",
|
|
1285
|
+
output_model=ImplicatedUsersArtifact,
|
|
1286
|
+
context_payload={
|
|
1287
|
+
"idea_id": event.idea_id,
|
|
1288
|
+
"implemented_idea": event.implemented_idea,
|
|
1289
|
+
"implemented_stories": event.implemented_stories,
|
|
1290
|
+
"code_evidence": event.code_evidence,
|
|
1291
|
+
"source_docs": event.source_docs,
|
|
1292
|
+
"canonical_actor_allowed_combinations": sorted(list(CANONICAL_ACTOR_ALLOWED_COMBINATIONS)),
|
|
1293
|
+
},
|
|
1294
|
+
guidance=[],
|
|
1295
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1296
|
+
)
|
|
1297
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="resolve_side_effects", envelope=side_effects_envelope)
|
|
1298
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="resolve_implicated_users", envelope=users_envelope)
|
|
1299
|
+
|
|
1300
|
+
side_effects_path, _ = _write_integration_artifact(
|
|
1301
|
+
repo_root=repo_root,
|
|
1302
|
+
idea_id=event.idea_id,
|
|
1303
|
+
pipeline_dir=pipeline_dir,
|
|
1304
|
+
filename="side_effects.json",
|
|
1305
|
+
data=side_effects_artifact.model_dump(),
|
|
1306
|
+
)
|
|
1307
|
+
implicated_users_path, _ = _write_integration_artifact(
|
|
1308
|
+
repo_root=repo_root,
|
|
1309
|
+
idea_id=event.idea_id,
|
|
1310
|
+
pipeline_dir=pipeline_dir,
|
|
1311
|
+
filename="implicated_users.json",
|
|
1312
|
+
data=implicated_users_artifact.model_dump(),
|
|
1313
|
+
)
|
|
1314
|
+
task_context.metadata["side_effects_artifact"] = side_effects_artifact.model_dump()
|
|
1315
|
+
task_context.metadata["implicated_users_artifact"] = implicated_users_artifact.model_dump()
|
|
1316
|
+
task_context.metadata["artifacts"].update({
|
|
1317
|
+
"side_effects": str(side_effects_path),
|
|
1318
|
+
"implicated_users": str(implicated_users_path),
|
|
1319
|
+
})
|
|
1320
|
+
self.save_output({
|
|
1321
|
+
"side_effects_ref": str(side_effects_path),
|
|
1322
|
+
"implicated_users_ref": str(implicated_users_path),
|
|
1323
|
+
})
|
|
1324
|
+
return task_context
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
class WriteWorkflowPerSideEffectNode(AgentNode):
|
|
1328
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1329
|
+
return AgentConfig(
|
|
1330
|
+
instructions=load_integration_node_instruction("write_workflows"),
|
|
1331
|
+
output_type=WorkflowSetArtifact,
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1335
|
+
# -- checkpoint/resume --
|
|
1336
|
+
prior_inv = _try_load_prior_artifact(task_context, "workflow_inventory.json")
|
|
1337
|
+
if prior_inv is not None:
|
|
1338
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1339
|
+
prior_se_ids = set(str(s) for s in (prior_inv.get("side_effect_ids") or []))
|
|
1340
|
+
current_se_ids = {
|
|
1341
|
+
str(item.get("id") or "")
|
|
1342
|
+
for item in (task_context.metadata.get("side_effects_artifact") or {}).get("side_effects") or []
|
|
1343
|
+
if str(item.get("id") or "")
|
|
1344
|
+
}
|
|
1345
|
+
prior_workflow_files = [current_dir / f"workflow_{_slug(str(se_id))}.json" for se_id in prior_se_ids]
|
|
1346
|
+
if prior_se_ids == current_se_ids and all(f.exists() for f in prior_workflow_files):
|
|
1347
|
+
workflows = []
|
|
1348
|
+
workflow_paths: dict[str, str] = {}
|
|
1349
|
+
for se_id in prior_se_ids:
|
|
1350
|
+
wf_path = current_dir / f"workflow_{_slug(str(se_id))}.json"
|
|
1351
|
+
wf = json.loads(wf_path.read_text(encoding="utf-8"))
|
|
1352
|
+
workflows.append(wf)
|
|
1353
|
+
workflow_paths[f"workflow_{_slug(str(se_id))}"] = str(wf_path)
|
|
1354
|
+
task_context.metadata["workflows"] = workflows
|
|
1355
|
+
task_context.metadata["artifacts"]["workflow_inventory"] = str(current_dir / "workflow_inventory.json")
|
|
1356
|
+
task_context.metadata["artifacts"].update(workflow_paths)
|
|
1357
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True, "workflow_count": len(workflows)})
|
|
1358
|
+
return task_context
|
|
1359
|
+
_abort_resume(task_context)
|
|
1360
|
+
# -- end checkpoint/resume --
|
|
1361
|
+
|
|
1362
|
+
_integration_dfs_progress(task_context, "Writing workflows")
|
|
1363
|
+
|
|
1364
|
+
event = task_context.event
|
|
1365
|
+
repo_root = Path(event.repo_root)
|
|
1366
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1367
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="write_workflows")
|
|
1368
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
1369
|
+
repo_root=repo_root,
|
|
1370
|
+
stage_name="write_workflows",
|
|
1371
|
+
output_model=WorkflowSetArtifact,
|
|
1372
|
+
context_payload={
|
|
1373
|
+
"idea_id": event.idea_id,
|
|
1374
|
+
"side_effects_artifact": task_context.metadata["side_effects_artifact"],
|
|
1375
|
+
"implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
|
|
1376
|
+
"implemented_stories": event.implemented_stories,
|
|
1377
|
+
"code_evidence": event.code_evidence,
|
|
1378
|
+
"source_docs": event.source_docs,
|
|
1379
|
+
},
|
|
1380
|
+
guidance=[],
|
|
1381
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1382
|
+
)
|
|
1383
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="write_workflows", envelope=envelope)
|
|
1384
|
+
|
|
1385
|
+
workflows = _ground_workflows_in_side_effects(
|
|
1386
|
+
workflows=[item.model_dump() for item in artifact.workflows],
|
|
1387
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1388
|
+
)
|
|
1389
|
+
workflow_inventory = {
|
|
1390
|
+
"idea_id": event.idea_id,
|
|
1391
|
+
"workflow_ids": [workflow["workflow_id"] for workflow in workflows],
|
|
1392
|
+
"side_effect_ids": [item.get("id") for item in task_context.metadata["side_effects_artifact"].get("side_effects") or []],
|
|
1393
|
+
"downstream_stages": [stage.node_id for stage in default_integration_stages()[2:]],
|
|
1394
|
+
}
|
|
1395
|
+
workflow_inventory_path, _ = _write_integration_artifact(
|
|
1396
|
+
repo_root=repo_root,
|
|
1397
|
+
idea_id=event.idea_id,
|
|
1398
|
+
pipeline_dir=pipeline_dir,
|
|
1399
|
+
filename="workflow_inventory.json",
|
|
1400
|
+
data=workflow_inventory,
|
|
1401
|
+
)
|
|
1402
|
+
task_context.metadata["artifacts"]["workflow_inventory"] = str(workflow_inventory_path)
|
|
1403
|
+
|
|
1404
|
+
workflow_paths: dict[str, str] = {}
|
|
1405
|
+
for workflow in workflows:
|
|
1406
|
+
path, _ = _write_integration_artifact(
|
|
1407
|
+
repo_root=repo_root,
|
|
1408
|
+
idea_id=event.idea_id,
|
|
1409
|
+
pipeline_dir=pipeline_dir,
|
|
1410
|
+
filename=f"workflow_{_slug(str(workflow['side_effect']['id']))}.json",
|
|
1411
|
+
data=workflow,
|
|
1412
|
+
)
|
|
1413
|
+
workflow_paths[workflow["workflow_id"]] = str(path)
|
|
1414
|
+
task_context.metadata["workflows"] = workflows
|
|
1415
|
+
task_context.metadata["artifacts"].update(workflow_paths)
|
|
1416
|
+
self.save_output({"workflow_inventory_ref": str(workflow_inventory_path), "workflow_count": len(workflows)})
|
|
1417
|
+
return task_context
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
class VegNode(Node):
|
|
1421
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1422
|
+
prior_gate = _try_load_prior_artifact(
|
|
1423
|
+
task_context,
|
|
1424
|
+
"validation_gate.json",
|
|
1425
|
+
"validation_gate_agentic_review.json",
|
|
1426
|
+
"idea_acceptance_coverage.json",
|
|
1427
|
+
"idea_acceptance_coverage.json",
|
|
1428
|
+
validity_check=lambda d: bool(d.get("passed")) or bool(d.get("structural_only")),
|
|
1429
|
+
)
|
|
1430
|
+
if prior_gate is not None:
|
|
1431
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1432
|
+
review_payload, review_path = _read_resume_artifact(task_context, "validation_gate_agentic_review.json")
|
|
1433
|
+
coverage_payload, coverage_path = _read_resume_artifact(task_context, "idea_acceptance_coverage.json")
|
|
1434
|
+
cluster_payload, cluster_path = _read_resume_artifact(task_context, "validation_repair_clusters.json")
|
|
1435
|
+
task_context.metadata["validation_report"] = prior_gate
|
|
1436
|
+
task_context.metadata["iterations_used"] = prior_gate.get("iterations_used", 1)
|
|
1437
|
+
task_context.metadata["builder_review"] = review_payload or {}
|
|
1438
|
+
task_context.metadata["idea_acceptance_coverage"] = coverage_payload or {}
|
|
1439
|
+
task_context.metadata["workflows"] = _ground_workflows_in_side_effects(
|
|
1440
|
+
workflows=list((review_payload or {}).get("enriched_workflows") or task_context.metadata.get("workflows") or []),
|
|
1441
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1442
|
+
)
|
|
1443
|
+
task_context.metadata["artifacts"]["validation_gate"] = str(current_dir / "validation_gate.json")
|
|
1444
|
+
if review_path is not None:
|
|
1445
|
+
task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
|
|
1446
|
+
if coverage_path is not None:
|
|
1447
|
+
task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
|
|
1448
|
+
if cluster_payload is not None:
|
|
1449
|
+
task_context.metadata["repair_clusters"] = cluster_payload
|
|
1450
|
+
if cluster_path is not None:
|
|
1451
|
+
task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
|
|
1452
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
|
|
1453
|
+
return task_context
|
|
1454
|
+
expected_fingerprint = str(task_context.metadata.get("resume_fingerprint") or "") or None
|
|
1455
|
+
failed_prior_gate: dict[str, Any] | None = None
|
|
1456
|
+
failed_gate_candidates = [_current_dir_from_task(task_context) / "validation_gate.json"]
|
|
1457
|
+
prior_run_dir = task_context.metadata.get("prior_run_dir")
|
|
1458
|
+
if prior_run_dir:
|
|
1459
|
+
failed_gate_candidates.append(Path(str(prior_run_dir)) / "validation_gate.json")
|
|
1460
|
+
for gate_path in failed_gate_candidates:
|
|
1461
|
+
if not gate_path.exists() or not _resume_dir_matches_fingerprint(run_dir=gate_path.parent, expected_fingerprint=expected_fingerprint):
|
|
1462
|
+
continue
|
|
1463
|
+
try:
|
|
1464
|
+
gate_payload = json.loads(gate_path.read_text(encoding="utf-8"))
|
|
1465
|
+
except Exception:
|
|
1466
|
+
continue
|
|
1467
|
+
if isinstance(gate_payload, dict) and not bool(gate_payload.get("passed")) and not bool(gate_payload.get("structural_only")):
|
|
1468
|
+
failed_prior_gate = gate_payload
|
|
1469
|
+
break
|
|
1470
|
+
if failed_prior_gate is not None:
|
|
1471
|
+
task_context.metadata["validation_report"] = failed_prior_gate
|
|
1472
|
+
_abort_resume(task_context)
|
|
1473
|
+
self.save_output({"resume_aborted": True, "reason": "prior veg gate failed"})
|
|
1474
|
+
return task_context
|
|
1475
|
+
_abort_resume(task_context)
|
|
1476
|
+
|
|
1477
|
+
_integration_dfs_progress(task_context, "VEG first pass")
|
|
1478
|
+
event = task_context.event
|
|
1479
|
+
repo_root = Path(event.repo_root)
|
|
1480
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1481
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="validate")
|
|
1482
|
+
|
|
1483
|
+
builder_artifact = _run_veg_idea_acceptance_builder(
|
|
1484
|
+
repo_root=repo_root,
|
|
1485
|
+
pipeline_dir=pipeline_dir,
|
|
1486
|
+
idea_id=event.idea_id,
|
|
1487
|
+
implemented_idea=event.implemented_idea,
|
|
1488
|
+
implemented_stories=event.implemented_stories,
|
|
1489
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1490
|
+
implicated_users_artifact=task_context.metadata["implicated_users_artifact"],
|
|
1491
|
+
workflows=task_context.metadata["workflows"],
|
|
1492
|
+
code_evidence=event.code_evidence,
|
|
1493
|
+
source_docs=event.source_docs,
|
|
1494
|
+
node_id="veg_builder_first_pass",
|
|
1495
|
+
)
|
|
1496
|
+
current_agentic_review = builder_artifact.model_dump()
|
|
1497
|
+
enriched_workflows = _ground_workflows_in_side_effects(
|
|
1498
|
+
workflows=[item.model_dump() for item in builder_artifact.enriched_workflows],
|
|
1499
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1500
|
+
)
|
|
1501
|
+
current_idea_acceptance_coverage = builder_artifact.idea_acceptance_coverage.model_dump()
|
|
1502
|
+
|
|
1503
|
+
total_interaction_points = sum(
|
|
1504
|
+
len(workflow.get("interaction_points") or [])
|
|
1505
|
+
for workflow in enriched_workflows
|
|
1506
|
+
)
|
|
1507
|
+
if total_interaction_points == 0:
|
|
1508
|
+
_integration_dfs_progress(task_context, "VEG structural only")
|
|
1509
|
+
workflow_count = len(enriched_workflows)
|
|
1510
|
+
review_path, _ = _write_integration_artifact(
|
|
1511
|
+
repo_root=repo_root,
|
|
1512
|
+
idea_id=event.idea_id,
|
|
1513
|
+
pipeline_dir=pipeline_dir,
|
|
1514
|
+
filename="validation_gate_agentic_review.json",
|
|
1515
|
+
data=current_agentic_review,
|
|
1516
|
+
)
|
|
1517
|
+
coverage_path, _ = _write_integration_artifact(
|
|
1518
|
+
repo_root=repo_root,
|
|
1519
|
+
idea_id=event.idea_id,
|
|
1520
|
+
pipeline_dir=pipeline_dir,
|
|
1521
|
+
filename="idea_acceptance_coverage.json",
|
|
1522
|
+
data=current_idea_acceptance_coverage,
|
|
1523
|
+
)
|
|
1524
|
+
validation_report = {
|
|
1525
|
+
"idea_id": event.idea_id,
|
|
1526
|
+
"passed": True,
|
|
1527
|
+
"structural_only": True,
|
|
1528
|
+
"reason": "no actual interaction points across workflows",
|
|
1529
|
+
"workflow_count": workflow_count,
|
|
1530
|
+
"interaction_point_count": 0,
|
|
1531
|
+
"agentic_review": {
|
|
1532
|
+
"summary": current_agentic_review.get("summary"),
|
|
1533
|
+
"findings": list(current_agentic_review.get("findings") or []),
|
|
1534
|
+
"blocking_findings": [
|
|
1535
|
+
finding
|
|
1536
|
+
for finding in list(current_agentic_review.get("findings") or [])
|
|
1537
|
+
if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
|
|
1538
|
+
],
|
|
1539
|
+
"enriched_workflow_count": len(current_agentic_review.get("enriched_workflows") or enriched_workflows),
|
|
1540
|
+
},
|
|
1541
|
+
"idea_acceptance_coverage": current_idea_acceptance_coverage,
|
|
1542
|
+
"repair_cycles": 0,
|
|
1543
|
+
"repair_patches_count": 0,
|
|
1544
|
+
}
|
|
1545
|
+
cluster_payload = {
|
|
1546
|
+
"idea_id": event.idea_id,
|
|
1547
|
+
"mode": "internal_sequential_clusters",
|
|
1548
|
+
"structural_only": True,
|
|
1549
|
+
"iterations": [],
|
|
1550
|
+
"latest_iteration": None,
|
|
1551
|
+
"unresolved_cluster_ids": [],
|
|
1552
|
+
}
|
|
1553
|
+
cluster_path, _ = _write_integration_artifact(
|
|
1554
|
+
repo_root=repo_root,
|
|
1555
|
+
idea_id=event.idea_id,
|
|
1556
|
+
pipeline_dir=pipeline_dir,
|
|
1557
|
+
filename="validation_repair_clusters.json",
|
|
1558
|
+
data=cluster_payload,
|
|
1559
|
+
)
|
|
1560
|
+
validation_path, _ = _write_integration_artifact(
|
|
1561
|
+
repo_root=repo_root,
|
|
1562
|
+
idea_id=event.idea_id,
|
|
1563
|
+
pipeline_dir=pipeline_dir,
|
|
1564
|
+
filename="validation_gate.json",
|
|
1565
|
+
data=validation_report,
|
|
1566
|
+
)
|
|
1567
|
+
task_context.metadata["structural_only"] = True
|
|
1568
|
+
task_context.metadata["iterations_used"] = 0
|
|
1569
|
+
task_context.metadata["validation_report"] = validation_report
|
|
1570
|
+
task_context.metadata["builder_review"] = current_agentic_review
|
|
1571
|
+
task_context.metadata["idea_acceptance_coverage"] = current_idea_acceptance_coverage
|
|
1572
|
+
task_context.metadata["repair_clusters"] = cluster_payload
|
|
1573
|
+
task_context.metadata["workflows"] = enriched_workflows
|
|
1574
|
+
task_context.metadata["artifacts"]["validation_gate"] = str(validation_path)
|
|
1575
|
+
task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
|
|
1576
|
+
task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
|
|
1577
|
+
task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
|
|
1578
|
+
self.save_output({
|
|
1579
|
+
"structural_only": True,
|
|
1580
|
+
"workflow_count": workflow_count,
|
|
1581
|
+
"interaction_point_count": 0,
|
|
1582
|
+
})
|
|
1583
|
+
task_context.stop_workflow()
|
|
1584
|
+
return task_context
|
|
1585
|
+
|
|
1586
|
+
_integration_dfs_progress(task_context, "VEG deterministic gate")
|
|
1587
|
+
report, iterations_used = _validate_workflows(
|
|
1588
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1589
|
+
implicated_users_artifact=task_context.metadata["implicated_users_artifact"],
|
|
1590
|
+
workflows=enriched_workflows,
|
|
1591
|
+
implemented_idea=event.implemented_idea,
|
|
1592
|
+
implemented_stories=event.implemented_stories,
|
|
1593
|
+
idea_acceptance_coverage=current_idea_acceptance_coverage,
|
|
1594
|
+
max_iterations=5,
|
|
1595
|
+
agentic_review=current_agentic_review,
|
|
1596
|
+
repo_root=repo_root,
|
|
1597
|
+
idea_id=event.idea_id,
|
|
1598
|
+
pipeline_dir=pipeline_dir,
|
|
1599
|
+
repair_node_id_prefix="veg",
|
|
1600
|
+
code_evidence=event.code_evidence,
|
|
1601
|
+
source_docs=event.source_docs,
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
latest_repair_clusters = dict(report.pop("_repair_clusters", {}) or {})
|
|
1605
|
+
latest_review = dict(report.pop("_full_agentic_review", current_agentic_review) or {})
|
|
1606
|
+
latest_workflows = _ground_workflows_in_side_effects(
|
|
1607
|
+
workflows=list(report.pop("_final_workflows", enriched_workflows) or enriched_workflows),
|
|
1608
|
+
side_effects_artifact=task_context.metadata["side_effects_artifact"],
|
|
1609
|
+
)
|
|
1610
|
+
latest_coverage = dict(report.get("idea_acceptance_coverage") or current_idea_acceptance_coverage)
|
|
1611
|
+
repair_cycles = max(0, int(iterations_used) - 1)
|
|
1612
|
+
report["repair_cycles"] = repair_cycles
|
|
1613
|
+
report["repair_patches_count"] = 0
|
|
1614
|
+
|
|
1615
|
+
review_path, _ = _write_integration_artifact(
|
|
1616
|
+
repo_root=repo_root,
|
|
1617
|
+
idea_id=event.idea_id,
|
|
1618
|
+
pipeline_dir=pipeline_dir,
|
|
1619
|
+
filename="validation_gate_agentic_review.json",
|
|
1620
|
+
data=latest_review,
|
|
1621
|
+
)
|
|
1622
|
+
coverage_path, _ = _write_integration_artifact(
|
|
1623
|
+
repo_root=repo_root,
|
|
1624
|
+
idea_id=event.idea_id,
|
|
1625
|
+
pipeline_dir=pipeline_dir,
|
|
1626
|
+
filename="idea_acceptance_coverage.json",
|
|
1627
|
+
data=latest_coverage,
|
|
1628
|
+
)
|
|
1629
|
+
cluster_path, _ = _write_integration_artifact(
|
|
1630
|
+
repo_root=repo_root,
|
|
1631
|
+
idea_id=event.idea_id,
|
|
1632
|
+
pipeline_dir=pipeline_dir,
|
|
1633
|
+
filename="validation_repair_clusters.json",
|
|
1634
|
+
data=latest_repair_clusters,
|
|
1635
|
+
)
|
|
1636
|
+
validation_path, _ = _write_integration_artifact(
|
|
1637
|
+
repo_root=repo_root,
|
|
1638
|
+
idea_id=event.idea_id,
|
|
1639
|
+
pipeline_dir=pipeline_dir,
|
|
1640
|
+
filename="validation_gate.json",
|
|
1641
|
+
data=report,
|
|
1642
|
+
)
|
|
1643
|
+
task_context.metadata["artifacts"]["validation_gate"] = str(validation_path)
|
|
1644
|
+
task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
|
|
1645
|
+
task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
|
|
1646
|
+
task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
|
|
1647
|
+
task_context.metadata["validation_report"] = report
|
|
1648
|
+
task_context.metadata["builder_review"] = latest_review
|
|
1649
|
+
task_context.metadata["idea_acceptance_coverage"] = latest_coverage
|
|
1650
|
+
task_context.metadata["repair_clusters"] = latest_repair_clusters
|
|
1651
|
+
task_context.metadata["workflows"] = latest_workflows
|
|
1652
|
+
task_context.metadata["iterations_used"] = iterations_used
|
|
1653
|
+
self.save_output(report)
|
|
1654
|
+
if not bool(report.get("passed")):
|
|
1655
|
+
task_context.stop_workflow()
|
|
1656
|
+
return task_context
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
def _canonicalize_red_artifact(*, workflows: list[dict[str, Any]], red_artifact: dict[str, Any]) -> dict[str, Any]:
|
|
1660
|
+
workflow_map = {str(item.get("workflow_id") or ""): item for item in workflows}
|
|
1661
|
+
normalized = json.loads(json.dumps(red_artifact))
|
|
1662
|
+
for package in normalized.get("packages") or []:
|
|
1663
|
+
workflow_id = str(package.get("workflow_id") or "")
|
|
1664
|
+
workflow = workflow_map.get(workflow_id)
|
|
1665
|
+
if workflow is None:
|
|
1666
|
+
continue
|
|
1667
|
+
harness = dict(package.get("harness_selection") or {})
|
|
1668
|
+
interaction_points = list(workflow.get("interaction_points") or [])
|
|
1669
|
+
needed_points = list(workflow.get("needed_interaction_points") or [])
|
|
1670
|
+
primary_point = (interaction_points or needed_points or [{}])[0]
|
|
1671
|
+
expected_harness = _preferred_harness_for_interaction_point(primary_point)
|
|
1672
|
+
if str(harness.get("harness_kind") or "") != expected_harness:
|
|
1673
|
+
harness["harness_kind"] = expected_harness
|
|
1674
|
+
package["harness_selection"] = harness
|
|
1675
|
+
return normalized
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _validate_red_packages(*, workflows: list[dict[str, Any]], red_artifact: dict[str, Any]) -> dict[str, Any]:
|
|
1679
|
+
workflow_map = {str(item.get("workflow_id") or ""): item for item in workflows}
|
|
1680
|
+
errors: list[str] = []
|
|
1681
|
+
for package in red_artifact.get("packages") or []:
|
|
1682
|
+
workflow_id = str(package.get("workflow_id") or "")
|
|
1683
|
+
workflow = workflow_map.get(workflow_id)
|
|
1684
|
+
if workflow is None:
|
|
1685
|
+
errors.append(f"red package references unknown workflow {workflow_id}")
|
|
1686
|
+
continue
|
|
1687
|
+
harness = dict(package.get("harness_selection") or {})
|
|
1688
|
+
actual_harness = str(harness.get("harness_kind") or "")
|
|
1689
|
+
interaction_points = list(workflow.get("interaction_points") or [])
|
|
1690
|
+
needed_points = list(workflow.get("needed_interaction_points") or [])
|
|
1691
|
+
primary_point = (interaction_points or needed_points or [{}])[0]
|
|
1692
|
+
expected_harness = _preferred_harness_for_interaction_point(primary_point)
|
|
1693
|
+
if not interaction_points and not needed_points and actual_harness == "artifact_probe":
|
|
1694
|
+
expected_harness = "artifact_probe"
|
|
1695
|
+
if actual_harness != expected_harness:
|
|
1696
|
+
errors.append(f"red harness mismatch for {workflow_id}: expected {expected_harness}")
|
|
1697
|
+
if not (package.get("expected_assertions") or []):
|
|
1698
|
+
errors.append(f"red expected assertions missing for {workflow_id}")
|
|
1699
|
+
if not interaction_points and needed_points and not bool(harness.get("gap_explicit")):
|
|
1700
|
+
errors.append(f"red gap not explicit for {workflow_id}")
|
|
1701
|
+
return {"passed": not errors, "errors": errors}
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
class RedNode(AgentNode):
|
|
1705
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1706
|
+
return AgentConfig(
|
|
1707
|
+
instructions=load_integration_node_instruction("red")
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1711
|
+
# -- checkpoint/resume --
|
|
1712
|
+
prior_red = _try_load_prior_artifact(task_context, "red_package.json", "red_validation.json")
|
|
1713
|
+
if prior_red is not None:
|
|
1714
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1715
|
+
prior_red_val, _ = _read_resume_artifact(task_context, "red_validation.json")
|
|
1716
|
+
if (prior_red_val or {}).get("passed"):
|
|
1717
|
+
task_context.metadata["red_artifact"] = prior_red
|
|
1718
|
+
task_context.metadata["artifacts"].update({
|
|
1719
|
+
"red_package": str(current_dir / "red_package.json"),
|
|
1720
|
+
"red_validation": str(current_dir / "red_validation.json"),
|
|
1721
|
+
})
|
|
1722
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
|
|
1723
|
+
return task_context
|
|
1724
|
+
_abort_resume(task_context)
|
|
1725
|
+
# -- end checkpoint/resume --
|
|
1726
|
+
|
|
1727
|
+
_integration_dfs_progress(task_context, "Generating tests")
|
|
1728
|
+
|
|
1729
|
+
event = task_context.event
|
|
1730
|
+
repo_root = Path(event.repo_root)
|
|
1731
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1732
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="red")
|
|
1733
|
+
|
|
1734
|
+
# Step 1: Run agent to get red artifact
|
|
1735
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
1736
|
+
repo_root=repo_root,
|
|
1737
|
+
stage_name="red",
|
|
1738
|
+
output_model=RedArtifact,
|
|
1739
|
+
context_payload={
|
|
1740
|
+
"idea_id": event.idea_id,
|
|
1741
|
+
"validated_workflows": task_context.metadata["workflows"],
|
|
1742
|
+
"validation_report": task_context.metadata["validation_report"],
|
|
1743
|
+
"side_effects_artifact": task_context.metadata["side_effects_artifact"],
|
|
1744
|
+
"implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
|
|
1745
|
+
"code_evidence": event.code_evidence,
|
|
1746
|
+
},
|
|
1747
|
+
guidance=[],
|
|
1748
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1749
|
+
)
|
|
1750
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="red", envelope=envelope)
|
|
1751
|
+
|
|
1752
|
+
# Step 2: Normalize and validate red packages
|
|
1753
|
+
red_data = _canonicalize_red_artifact(
|
|
1754
|
+
workflows=task_context.metadata["workflows"],
|
|
1755
|
+
red_artifact=artifact.model_dump(),
|
|
1756
|
+
)
|
|
1757
|
+
report = _validate_red_packages(workflows=task_context.metadata["workflows"], red_artifact=red_data)
|
|
1758
|
+
|
|
1759
|
+
# Step 3: If fails, enter repair loop (up to 2 iterations)
|
|
1760
|
+
if not report["passed"]:
|
|
1761
|
+
max_repair = 2
|
|
1762
|
+
for repair_iter in range(1, max_repair + 1):
|
|
1763
|
+
_integration_dfs_progress(task_context, f"Repair pass {repair_iter}/{max_repair}")
|
|
1764
|
+
# Identify failing packages
|
|
1765
|
+
failing_packages = []
|
|
1766
|
+
for pkg in red_data.get("packages") or []:
|
|
1767
|
+
wf_id = str(pkg.get("workflow_id") or "")
|
|
1768
|
+
if any(wf_id in err for err in report.get("errors") or []):
|
|
1769
|
+
failing_packages.append(pkg)
|
|
1770
|
+
if not failing_packages:
|
|
1771
|
+
failing_packages = red_data.get("packages") or []
|
|
1772
|
+
|
|
1773
|
+
failing_workflows = [
|
|
1774
|
+
wf for wf in task_context.metadata["workflows"]
|
|
1775
|
+
if str(wf.get("workflow_id") or "") in {str(p.get("workflow_id") or "") for p in failing_packages}
|
|
1776
|
+
]
|
|
1777
|
+
|
|
1778
|
+
# Call repair agent
|
|
1779
|
+
repair_artifact, repair_envelope = integration_agentic.run_integration_code_repair_step(
|
|
1780
|
+
repo_root=repo_root,
|
|
1781
|
+
output_model=CodeRepairArtifact,
|
|
1782
|
+
context_payload={
|
|
1783
|
+
"failing_packages": failing_packages,
|
|
1784
|
+
"validation_errors": report.get("errors") or [],
|
|
1785
|
+
"failing_workflows": failing_workflows,
|
|
1786
|
+
"side_effects_artifact": task_context.metadata["side_effects_artifact"],
|
|
1787
|
+
"implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
|
|
1788
|
+
"code_evidence": event.code_evidence,
|
|
1789
|
+
},
|
|
1790
|
+
iteration=repair_iter,
|
|
1791
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1792
|
+
)
|
|
1793
|
+
integration_agentic.persist_agent_run(
|
|
1794
|
+
pipeline_root=pipeline_dir,
|
|
1795
|
+
node_id=f"red_code_repair_iter{repair_iter}",
|
|
1796
|
+
envelope=repair_envelope,
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
# Apply file patches
|
|
1800
|
+
for patch in repair_artifact.patches:
|
|
1801
|
+
patch_path = repo_root / patch.file_path
|
|
1802
|
+
patch_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1803
|
+
patch_path.write_text(patch.content, encoding="utf-8")
|
|
1804
|
+
|
|
1805
|
+
# Re-run agent step for failing workflows to re-generate their red packages
|
|
1806
|
+
_integration_dfs_progress(task_context, f"Re-running tests {repair_iter}/{max_repair}")
|
|
1807
|
+
re_artifact, re_envelope = integration_agentic.run_integration_agent_step(
|
|
1808
|
+
repo_root=repo_root,
|
|
1809
|
+
stage_name=f"red_repair_iter{repair_iter}",
|
|
1810
|
+
output_model=RedArtifact,
|
|
1811
|
+
context_payload={
|
|
1812
|
+
"idea_id": event.idea_id,
|
|
1813
|
+
"validated_workflows": failing_workflows,
|
|
1814
|
+
"validation_report": task_context.metadata["validation_report"],
|
|
1815
|
+
"side_effects_artifact": task_context.metadata["side_effects_artifact"],
|
|
1816
|
+
"implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
|
|
1817
|
+
"code_evidence": event.code_evidence,
|
|
1818
|
+
},
|
|
1819
|
+
guidance=[],
|
|
1820
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
# Merge re-generated packages back into red_data
|
|
1824
|
+
re_normalized = _canonicalize_red_artifact(
|
|
1825
|
+
workflows=failing_workflows,
|
|
1826
|
+
red_artifact=re_artifact.model_dump(),
|
|
1827
|
+
)
|
|
1828
|
+
re_pkg_map = {str(p.get("workflow_id") or ""): p for p in re_normalized.get("packages") or []}
|
|
1829
|
+
new_packages = []
|
|
1830
|
+
for pkg in red_data.get("packages") or []:
|
|
1831
|
+
wf_id = str(pkg.get("workflow_id") or "")
|
|
1832
|
+
new_packages.append(re_pkg_map.pop(wf_id, pkg))
|
|
1833
|
+
for leftover in re_pkg_map.values():
|
|
1834
|
+
new_packages.append(leftover)
|
|
1835
|
+
red_data["packages"] = new_packages
|
|
1836
|
+
|
|
1837
|
+
# Re-validate
|
|
1838
|
+
report = _validate_red_packages(workflows=task_context.metadata["workflows"], red_artifact=red_data)
|
|
1839
|
+
if report["passed"]:
|
|
1840
|
+
break
|
|
1841
|
+
|
|
1842
|
+
# Step 4: Write artifacts
|
|
1843
|
+
if not report["passed"]:
|
|
1844
|
+
task_context.stop_workflow()
|
|
1845
|
+
red_path, _ = _write_integration_artifact(
|
|
1846
|
+
repo_root=repo_root,
|
|
1847
|
+
idea_id=event.idea_id,
|
|
1848
|
+
pipeline_dir=pipeline_dir,
|
|
1849
|
+
filename="red_package.json",
|
|
1850
|
+
data=red_data,
|
|
1851
|
+
)
|
|
1852
|
+
red_validation_path, _ = _write_integration_artifact(
|
|
1853
|
+
repo_root=repo_root,
|
|
1854
|
+
idea_id=event.idea_id,
|
|
1855
|
+
pipeline_dir=pipeline_dir,
|
|
1856
|
+
filename="red_validation.json",
|
|
1857
|
+
data=report,
|
|
1858
|
+
)
|
|
1859
|
+
task_context.metadata["red_artifact"] = red_data
|
|
1860
|
+
task_context.metadata["artifacts"].update({
|
|
1861
|
+
"red_package": str(red_path),
|
|
1862
|
+
"red_validation": str(red_validation_path),
|
|
1863
|
+
})
|
|
1864
|
+
self.save_output({"red_package_ref": str(red_path), "validation": report})
|
|
1865
|
+
return task_context
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
class RedReviewNode(AgentNode):
|
|
1869
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1870
|
+
return AgentConfig(
|
|
1871
|
+
instructions=load_integration_node_instruction("red_review")
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1875
|
+
# -- checkpoint/resume --
|
|
1876
|
+
prior_rr = _try_load_prior_artifact(
|
|
1877
|
+
task_context, "red_review.json",
|
|
1878
|
+
validity_check=lambda d: bool(d.get("agentic_review_clear")),
|
|
1879
|
+
)
|
|
1880
|
+
if prior_rr is not None:
|
|
1881
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1882
|
+
task_context.metadata["artifacts"]["red_review"] = str(current_dir / "red_review.json")
|
|
1883
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
|
|
1884
|
+
return task_context
|
|
1885
|
+
_abort_resume(task_context)
|
|
1886
|
+
# -- end checkpoint/resume --
|
|
1887
|
+
|
|
1888
|
+
_integration_dfs_progress(task_context, "Reviewing tests")
|
|
1889
|
+
|
|
1890
|
+
event = task_context.event
|
|
1891
|
+
repo_root = Path(event.repo_root)
|
|
1892
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1893
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="red_review")
|
|
1894
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
1895
|
+
repo_root=repo_root,
|
|
1896
|
+
stage_name="red_review",
|
|
1897
|
+
output_model=RedReviewArtifact,
|
|
1898
|
+
context_payload={
|
|
1899
|
+
"idea_id": event.idea_id,
|
|
1900
|
+
"validated_workflows": task_context.metadata["workflows"],
|
|
1901
|
+
"red_artifact": task_context.metadata["red_artifact"],
|
|
1902
|
+
"validation_report": task_context.metadata["validation_report"],
|
|
1903
|
+
},
|
|
1904
|
+
guidance=[],
|
|
1905
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1906
|
+
)
|
|
1907
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="redreview", envelope=envelope)
|
|
1908
|
+
path, _ = _write_integration_artifact(
|
|
1909
|
+
repo_root=repo_root,
|
|
1910
|
+
idea_id=event.idea_id,
|
|
1911
|
+
pipeline_dir=pipeline_dir,
|
|
1912
|
+
filename="red_review.json",
|
|
1913
|
+
data=artifact.model_dump(),
|
|
1914
|
+
)
|
|
1915
|
+
task_context.metadata["red_review_artifact"] = artifact.model_dump()
|
|
1916
|
+
task_context.metadata["artifacts"]["red_review"] = str(path)
|
|
1917
|
+
self.save_output({"red_review_ref": str(path)})
|
|
1918
|
+
return task_context
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
class GreenNode(AgentNode):
|
|
1922
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1923
|
+
return AgentConfig(
|
|
1924
|
+
instructions=load_integration_node_instruction("green")
|
|
1925
|
+
)
|
|
1926
|
+
|
|
1927
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1928
|
+
# -- checkpoint/resume --
|
|
1929
|
+
prior_green = _try_load_prior_artifact(task_context, "green_package.json")
|
|
1930
|
+
if prior_green is not None:
|
|
1931
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1932
|
+
task_context.metadata["artifacts"]["green_package"] = str(current_dir / "green_package.json")
|
|
1933
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
|
|
1934
|
+
return task_context
|
|
1935
|
+
_abort_resume(task_context)
|
|
1936
|
+
# -- end checkpoint/resume --
|
|
1937
|
+
|
|
1938
|
+
_integration_dfs_progress(task_context, "Writing green tests")
|
|
1939
|
+
|
|
1940
|
+
event = task_context.event
|
|
1941
|
+
repo_root = Path(event.repo_root)
|
|
1942
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1943
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="green")
|
|
1944
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
1945
|
+
repo_root=repo_root,
|
|
1946
|
+
stage_name="green",
|
|
1947
|
+
output_model=GreenArtifact,
|
|
1948
|
+
context_payload={
|
|
1949
|
+
"idea_id": event.idea_id,
|
|
1950
|
+
"validated_workflows": task_context.metadata["workflows"],
|
|
1951
|
+
"red_review_artifact": task_context.metadata["red_review_artifact"],
|
|
1952
|
+
"code_evidence": event.code_evidence,
|
|
1953
|
+
"implemented_stories": event.implemented_stories,
|
|
1954
|
+
},
|
|
1955
|
+
guidance=[],
|
|
1956
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
1957
|
+
)
|
|
1958
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="green", envelope=envelope)
|
|
1959
|
+
path, _ = _write_integration_artifact(
|
|
1960
|
+
repo_root=repo_root,
|
|
1961
|
+
idea_id=event.idea_id,
|
|
1962
|
+
pipeline_dir=pipeline_dir,
|
|
1963
|
+
filename="green_package.json",
|
|
1964
|
+
data=artifact.model_dump(),
|
|
1965
|
+
)
|
|
1966
|
+
task_context.metadata["green_artifact"] = artifact.model_dump()
|
|
1967
|
+
task_context.metadata["artifacts"]["green_package"] = str(path)
|
|
1968
|
+
self.save_output({"green_package_ref": str(path)})
|
|
1969
|
+
return task_context
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
class GreenEnrichNode(AgentNode):
|
|
1973
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1974
|
+
return AgentConfig(
|
|
1975
|
+
instructions=load_integration_node_instruction("green_enrich")
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1979
|
+
# -- checkpoint/resume --
|
|
1980
|
+
prior_ge = _try_load_prior_artifact(task_context, "green_enrich.json")
|
|
1981
|
+
if prior_ge is not None:
|
|
1982
|
+
current_dir = _current_dir_from_task(task_context)
|
|
1983
|
+
task_context.metadata["artifacts"]["green_enrich"] = str(current_dir / "green_enrich.json")
|
|
1984
|
+
self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
|
|
1985
|
+
return task_context
|
|
1986
|
+
_abort_resume(task_context)
|
|
1987
|
+
# -- end checkpoint/resume --
|
|
1988
|
+
|
|
1989
|
+
_integration_dfs_progress(task_context, "Enriching tests")
|
|
1990
|
+
|
|
1991
|
+
event = task_context.event
|
|
1992
|
+
repo_root = Path(event.repo_root)
|
|
1993
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
1994
|
+
_invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="green_enrich")
|
|
1995
|
+
artifact, envelope = integration_agentic.run_integration_agent_step(
|
|
1996
|
+
repo_root=repo_root,
|
|
1997
|
+
stage_name="green_enrich",
|
|
1998
|
+
output_model=GreenEnrichArtifact,
|
|
1999
|
+
context_payload={
|
|
2000
|
+
"idea_id": event.idea_id,
|
|
2001
|
+
"validated_workflows": task_context.metadata["workflows"],
|
|
2002
|
+
"green_artifact": task_context.metadata["green_artifact"],
|
|
2003
|
+
"red_review_artifact": task_context.metadata["red_review_artifact"],
|
|
2004
|
+
},
|
|
2005
|
+
guidance=[],
|
|
2006
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
2007
|
+
)
|
|
2008
|
+
integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="greenenrich", envelope=envelope)
|
|
2009
|
+
path, _ = _write_integration_artifact(
|
|
2010
|
+
repo_root=repo_root,
|
|
2011
|
+
idea_id=event.idea_id,
|
|
2012
|
+
pipeline_dir=pipeline_dir,
|
|
2013
|
+
filename="green_enrich.json",
|
|
2014
|
+
data=artifact.model_dump(),
|
|
2015
|
+
)
|
|
2016
|
+
task_context.metadata["green_enrich_artifact"] = artifact.model_dump()
|
|
2017
|
+
task_context.metadata["artifacts"]["green_enrich"] = str(path)
|
|
2018
|
+
self.save_output({"green_enrich_ref": str(path)})
|
|
2019
|
+
return task_context
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
class CommitNode(Node):
|
|
2023
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
2024
|
+
_integration_dfs_progress(task_context, "Committing")
|
|
2025
|
+
event = task_context.event
|
|
2026
|
+
pipeline_dir = Path(task_context.metadata["pipeline_dir"])
|
|
2027
|
+
workflows = list(task_context.metadata.get("workflows") or [])
|
|
2028
|
+
green_packages = {
|
|
2029
|
+
str(item.get("workflow_id") or ""): item
|
|
2030
|
+
for item in (task_context.metadata.get("green_artifact") or {}).get("packages") or []
|
|
2031
|
+
}
|
|
2032
|
+
enrich_packages = {
|
|
2033
|
+
str(item.get("workflow_id") or ""): item
|
|
2034
|
+
for item in (task_context.metadata.get("green_enrich_artifact") or {}).get("packages") or []
|
|
2035
|
+
}
|
|
2036
|
+
playwright_request = _build_post_integration_playwright_request(
|
|
2037
|
+
idea_id=event.idea_id,
|
|
2038
|
+
trigger_run_id=str(task_context.metadata.get("dag_run_id") or event.dag_run_id or "") or None,
|
|
2039
|
+
workflows=workflows,
|
|
2040
|
+
red_artifact=dict(task_context.metadata.get("red_artifact") or {}),
|
|
2041
|
+
)
|
|
2042
|
+
request_path, _ = _write_integration_artifact(
|
|
2043
|
+
repo_root=Path(event.repo_root),
|
|
2044
|
+
idea_id=event.idea_id,
|
|
2045
|
+
pipeline_dir=pipeline_dir,
|
|
2046
|
+
filename="post_integration_playwright_request.json",
|
|
2047
|
+
data=playwright_request,
|
|
2048
|
+
)
|
|
2049
|
+
task_context.metadata["post_integration_playwright_request"] = playwright_request
|
|
2050
|
+
task_context.metadata["artifacts"]["post_integration_playwright_request"] = str(request_path)
|
|
2051
|
+
commit_packages: list[CommitWorkflowPackage] = []
|
|
2052
|
+
for workflow in workflows:
|
|
2053
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
2054
|
+
commit_packages.append(
|
|
2055
|
+
CommitWorkflowPackage(
|
|
2056
|
+
workflow_id=workflow_id,
|
|
2057
|
+
side_effect_id=str(workflow.get("side_effect", {}).get("id") or ""),
|
|
2058
|
+
included_artifacts=[
|
|
2059
|
+
"validation_gate.json",
|
|
2060
|
+
"red_package.json",
|
|
2061
|
+
"red_review.json",
|
|
2062
|
+
"green_package.json",
|
|
2063
|
+
"green_enrich.json",
|
|
2064
|
+
"post_integration_playwright_request.json",
|
|
2065
|
+
],
|
|
2066
|
+
traceability={
|
|
2067
|
+
"idea_id": event.idea_id,
|
|
2068
|
+
"workflow_ref": workflow_id,
|
|
2069
|
+
"side_effect_ref": workflow.get("side_effect", {}).get("id"),
|
|
2070
|
+
"story_backing": workflow.get("story_backing") or [],
|
|
2071
|
+
"green_changes": (green_packages.get(workflow_id) or {}).get("implementation_changes") or [],
|
|
2072
|
+
"green_enrich_assertions": (
|
|
2073
|
+
enrich_packages.get(workflow_id) or {}
|
|
2074
|
+
).get("strengthened_assertions") or [],
|
|
2075
|
+
},
|
|
2076
|
+
)
|
|
2077
|
+
)
|
|
2078
|
+
artifact = CommitArtifact(
|
|
2079
|
+
idea_id=event.idea_id,
|
|
2080
|
+
summary="Integration-proof package assembled and traceably linked.",
|
|
2081
|
+
packages=commit_packages,
|
|
2082
|
+
package_manifest={
|
|
2083
|
+
"idea_id": event.idea_id,
|
|
2084
|
+
"pipeline_dir": str(pipeline_dir),
|
|
2085
|
+
"workflow_ids": [item.workflow_id for item in commit_packages],
|
|
2086
|
+
"artifact_refs": dict(task_context.metadata.get("artifacts") or {}),
|
|
2087
|
+
"post_integration_playwright": {
|
|
2088
|
+
"status": playwright_request["status"],
|
|
2089
|
+
"required_workflow_ids": playwright_request["required_workflow_ids"],
|
|
2090
|
+
"request_ref": str(request_path),
|
|
2091
|
+
},
|
|
2092
|
+
},
|
|
2093
|
+
)
|
|
2094
|
+
path, _ = _write_integration_artifact(
|
|
2095
|
+
repo_root=Path(event.repo_root),
|
|
2096
|
+
idea_id=event.idea_id,
|
|
2097
|
+
pipeline_dir=pipeline_dir,
|
|
2098
|
+
filename="commit_package.json",
|
|
2099
|
+
data=artifact.model_dump(),
|
|
2100
|
+
)
|
|
2101
|
+
task_context.metadata["commit_artifact"] = artifact.model_dump()
|
|
2102
|
+
task_context.metadata["artifacts"]["commit_package"] = str(path)
|
|
2103
|
+
self.save_output({"commit_package_ref": str(path), "workflow_count": len(commit_packages)})
|
|
2104
|
+
return task_context
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
class IntegrationWorkflow(Workflow):
|
|
2108
|
+
workflow_schema = WorkflowSchema(
|
|
2109
|
+
description="Integration DAG (agentic side-effect resolution -> workflow writing -> single hybrid VEG node with internal deterministic gate and repair loop -> real downstream red/redreview/green/greenenrich/commit chain)",
|
|
2110
|
+
event_schema=IntegrationDagEvent,
|
|
2111
|
+
start=LoadIntegrationContextNode,
|
|
2112
|
+
nodes=[
|
|
2113
|
+
NodeConfig(node=LoadIntegrationContextNode, connections=[ResolveSideEffectsAndImplicatedUsersNode]),
|
|
2114
|
+
NodeConfig(node=ResolveSideEffectsAndImplicatedUsersNode, connections=[WriteWorkflowPerSideEffectNode]),
|
|
2115
|
+
NodeConfig(node=WriteWorkflowPerSideEffectNode, connections=[VegNode]),
|
|
2116
|
+
NodeConfig(node=VegNode, connections=[RedNode]),
|
|
2117
|
+
NodeConfig(node=RedNode, connections=[RedReviewNode]),
|
|
2118
|
+
NodeConfig(node=RedReviewNode, connections=[GreenNode]),
|
|
2119
|
+
NodeConfig(node=GreenNode, connections=[GreenEnrichNode]),
|
|
2120
|
+
NodeConfig(node=GreenEnrichNode, connections=[CommitNode]),
|
|
2121
|
+
NodeConfig(node=CommitNode, connections=[]),
|
|
2122
|
+
],
|
|
2123
|
+
)
|
|
2124
|
+
|
|
2125
|
+
|
|
2126
|
+
def _workflow_story_ids(workflow: dict[str, Any]) -> list[str]:
|
|
2127
|
+
return [
|
|
2128
|
+
str(item.get("story_id") or "").strip()
|
|
2129
|
+
for item in (workflow.get("story_backing") or [])
|
|
2130
|
+
if str(item.get("story_id") or "").strip()
|
|
2131
|
+
]
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
def _extract_acceptance_criteria(value: Any) -> list[str]:
|
|
2135
|
+
if isinstance(value, list):
|
|
2136
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
2137
|
+
text = str(value or "").strip()
|
|
2138
|
+
return [text] if text else []
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _story_acceptance_index(implemented_stories: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
2142
|
+
index: dict[str, dict[str, Any]] = {}
|
|
2143
|
+
for story in implemented_stories:
|
|
2144
|
+
story_id = str(story.get("story_id") or story.get("id") or "").strip()
|
|
2145
|
+
if not story_id:
|
|
2146
|
+
continue
|
|
2147
|
+
criteria = _extract_acceptance_criteria(story.get("acceptance_criteria"))
|
|
2148
|
+
if not criteria:
|
|
2149
|
+
continue
|
|
2150
|
+
index[story_id] = {
|
|
2151
|
+
"story_id": story_id,
|
|
2152
|
+
"title": str(story.get("title") or story.get("name") or story_id).strip(),
|
|
2153
|
+
"acceptance_criteria": criteria,
|
|
2154
|
+
"side_effect_ids": [str(item).strip() for item in (story.get("side_effect_ids") or []) if str(item).strip()],
|
|
2155
|
+
}
|
|
2156
|
+
return index
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
def _idea_acceptance_criteria(implemented_idea: dict[str, Any] | None) -> list[str]:
|
|
2160
|
+
if not isinstance(implemented_idea, dict):
|
|
2161
|
+
return []
|
|
2162
|
+
idea_block = implemented_idea.get("idea") if isinstance(implemented_idea.get("idea"), dict) else implemented_idea
|
|
2163
|
+
return _extract_acceptance_criteria((idea_block or {}).get("acceptance_criteria"))
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
def _determine_failing_workflow_ids(
|
|
2167
|
+
*,
|
|
2168
|
+
workflows: list[dict[str, Any]],
|
|
2169
|
+
errors: list[str],
|
|
2170
|
+
blocking_agentic_findings: list[dict[str, Any]],
|
|
2171
|
+
repair_sources: list[dict[str, Any]] | None = None,
|
|
2172
|
+
) -> set[str]:
|
|
2173
|
+
workflow_ids = {str(workflow.get("workflow_id") or "") for workflow in workflows if str(workflow.get("workflow_id") or "")}
|
|
2174
|
+
side_effect_to_workflow_ids: dict[str, set[str]] = {}
|
|
2175
|
+
story_to_workflow_ids: dict[str, set[str]] = {}
|
|
2176
|
+
for workflow in workflows:
|
|
2177
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
2178
|
+
if not workflow_id:
|
|
2179
|
+
continue
|
|
2180
|
+
side_effect_id = str(workflow.get("side_effect", {}).get("id") or "")
|
|
2181
|
+
if side_effect_id:
|
|
2182
|
+
side_effect_to_workflow_ids.setdefault(side_effect_id, set()).add(workflow_id)
|
|
2183
|
+
for story_id in _workflow_story_ids(workflow):
|
|
2184
|
+
story_to_workflow_ids.setdefault(story_id, set()).add(workflow_id)
|
|
2185
|
+
|
|
2186
|
+
failing: set[str] = set()
|
|
2187
|
+
for error in errors:
|
|
2188
|
+
for workflow_id in workflow_ids:
|
|
2189
|
+
if workflow_id and workflow_id in error:
|
|
2190
|
+
failing.add(workflow_id)
|
|
2191
|
+
for side_effect_id, mapped_ids in side_effect_to_workflow_ids.items():
|
|
2192
|
+
if side_effect_id and side_effect_id in error:
|
|
2193
|
+
failing.update(mapped_ids)
|
|
2194
|
+
for story_id, mapped_ids in story_to_workflow_ids.items():
|
|
2195
|
+
if story_id and story_id in error:
|
|
2196
|
+
failing.update(mapped_ids)
|
|
2197
|
+
for finding in blocking_agentic_findings:
|
|
2198
|
+
workflow_id = str(finding.get("workflow_id") or "").strip()
|
|
2199
|
+
if workflow_id:
|
|
2200
|
+
failing.add(workflow_id)
|
|
2201
|
+
for source in repair_sources or []:
|
|
2202
|
+
for workflow_id in source.get("workflow_ids") or []:
|
|
2203
|
+
workflow_id = str(workflow_id or "").strip()
|
|
2204
|
+
if workflow_id:
|
|
2205
|
+
failing.add(workflow_id)
|
|
2206
|
+
return failing
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
def _idea_acceptance_lineage_is_model_backed(lineage: Any) -> bool:
|
|
2210
|
+
if not isinstance(lineage, dict):
|
|
2211
|
+
return False
|
|
2212
|
+
if bool(lineage.get("model_backed")):
|
|
2213
|
+
return True
|
|
2214
|
+
metadata = lineage.get("metadata")
|
|
2215
|
+
return isinstance(metadata, dict) and bool(metadata.get("model_backed"))
|
|
2216
|
+
|
|
2217
|
+
|
|
2218
|
+
def _normalize_idea_acceptance_coverage(
|
|
2219
|
+
*,
|
|
2220
|
+
idea_acceptance_coverage: dict[str, Any] | None,
|
|
2221
|
+
canonical_criteria: list[str],
|
|
2222
|
+
) -> dict[str, Any]:
|
|
2223
|
+
coverage = dict(idea_acceptance_coverage or {})
|
|
2224
|
+
raw_entries = [dict(entry) for entry in (coverage.get("criteria") or []) if isinstance(entry, dict)]
|
|
2225
|
+
if not canonical_criteria:
|
|
2226
|
+
coverage["criteria"] = []
|
|
2227
|
+
return coverage
|
|
2228
|
+
|
|
2229
|
+
normalized_entries: list[dict[str, Any]] = []
|
|
2230
|
+
used_positions: set[int] = set()
|
|
2231
|
+
criterion_to_index = {
|
|
2232
|
+
criterion: idx
|
|
2233
|
+
for idx, criterion in enumerate(canonical_criteria, start=1)
|
|
2234
|
+
}
|
|
2235
|
+
for idx, canonical_criterion in enumerate(canonical_criteria, start=1):
|
|
2236
|
+
selected_position: int | None = None
|
|
2237
|
+
fallback_position: int | None = None
|
|
2238
|
+
for position, entry in enumerate(raw_entries):
|
|
2239
|
+
if position in used_positions:
|
|
2240
|
+
continue
|
|
2241
|
+
entry_index = int(entry.get("criterion_index") or 0)
|
|
2242
|
+
entry_criterion = str(entry.get("criterion") or "").strip()
|
|
2243
|
+
if entry_index == idx and entry_criterion == canonical_criterion:
|
|
2244
|
+
selected_position = position
|
|
2245
|
+
break
|
|
2246
|
+
if fallback_position is None and entry_criterion == canonical_criterion:
|
|
2247
|
+
fallback_position = position
|
|
2248
|
+
elif fallback_position is None and entry_index == idx:
|
|
2249
|
+
fallback_position = position
|
|
2250
|
+
if selected_position is None:
|
|
2251
|
+
selected_position = fallback_position
|
|
2252
|
+
if selected_position is None:
|
|
2253
|
+
continue
|
|
2254
|
+
used_positions.add(selected_position)
|
|
2255
|
+
normalized_entry = dict(raw_entries[selected_position])
|
|
2256
|
+
normalized_entry["criterion_index"] = idx
|
|
2257
|
+
normalized_entry["criterion"] = canonical_criterion
|
|
2258
|
+
normalized_entries.append(normalized_entry)
|
|
2259
|
+
|
|
2260
|
+
for position, entry in enumerate(raw_entries):
|
|
2261
|
+
if position in used_positions:
|
|
2262
|
+
continue
|
|
2263
|
+
entry_criterion = str(entry.get("criterion") or "").strip()
|
|
2264
|
+
mapped_index = criterion_to_index.get(entry_criterion)
|
|
2265
|
+
if mapped_index is None:
|
|
2266
|
+
continue
|
|
2267
|
+
if any(int(item.get("criterion_index") or 0) == mapped_index for item in normalized_entries):
|
|
2268
|
+
continue
|
|
2269
|
+
normalized_entry = dict(entry)
|
|
2270
|
+
normalized_entry["criterion_index"] = mapped_index
|
|
2271
|
+
normalized_entry["criterion"] = canonical_criteria[mapped_index - 1]
|
|
2272
|
+
normalized_entries.append(normalized_entry)
|
|
2273
|
+
|
|
2274
|
+
normalized_entries.sort(key=lambda entry: int(entry.get("criterion_index") or 0))
|
|
2275
|
+
coverage["criteria"] = normalized_entries
|
|
2276
|
+
return coverage
|
|
2277
|
+
|
|
2278
|
+
|
|
2279
|
+
def _filter_idea_acceptance_coverage(
|
|
2280
|
+
*,
|
|
2281
|
+
idea_acceptance_coverage: dict[str, Any] | None,
|
|
2282
|
+
criterion_indices: set[int],
|
|
2283
|
+
) -> dict[str, Any]:
|
|
2284
|
+
filtered = dict(idea_acceptance_coverage or {})
|
|
2285
|
+
filtered["criteria"] = [
|
|
2286
|
+
dict(entry)
|
|
2287
|
+
for entry in (filtered.get("criteria") or [])
|
|
2288
|
+
if int(entry.get("criterion_index") or 0) in criterion_indices
|
|
2289
|
+
]
|
|
2290
|
+
return filtered
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
def _validation_convergence_state(
|
|
2294
|
+
*,
|
|
2295
|
+
idea_acceptance_coverage: dict[str, Any] | None,
|
|
2296
|
+
deterministic_errors: list[str],
|
|
2297
|
+
blocking_agentic_findings: list[dict[str, Any]],
|
|
2298
|
+
acceptance_failures: list[dict[str, Any]],
|
|
2299
|
+
idea_acceptance_failures: list[dict[str, Any]],
|
|
2300
|
+
) -> dict[str, Any]:
|
|
2301
|
+
verdict_map = {
|
|
2302
|
+
int(entry.get("criterion_index") or 0): str(entry.get("verdict") or "").strip().lower()
|
|
2303
|
+
for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
|
|
2304
|
+
if int(entry.get("criterion_index") or 0) > 0
|
|
2305
|
+
}
|
|
2306
|
+
finding_set = sorted({
|
|
2307
|
+
*[str(error).strip() for error in deterministic_errors if str(error).strip()],
|
|
2308
|
+
*[
|
|
2309
|
+
f"{str(finding.get('workflow_id') or 'global').strip()}::{str(finding.get('summary') or '').strip()}"
|
|
2310
|
+
for finding in blocking_agentic_findings
|
|
2311
|
+
if str(finding.get("summary") or "").strip()
|
|
2312
|
+
],
|
|
2313
|
+
})
|
|
2314
|
+
missing_seams = sorted({
|
|
2315
|
+
*[
|
|
2316
|
+
f"criterion#{int(entry.get('criterion_index') or 0)}::{seam}"
|
|
2317
|
+
for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
|
|
2318
|
+
for seam in list(entry.get("missing_seams") or [])
|
|
2319
|
+
if int(entry.get("criterion_index") or 0) > 0 and str(seam).strip()
|
|
2320
|
+
],
|
|
2321
|
+
*[
|
|
2322
|
+
str(failure.get("message") or "").strip()
|
|
2323
|
+
for failure in acceptance_failures
|
|
2324
|
+
if str(failure.get("source_type") or "").strip() == "acceptance_criterion_missing_seam"
|
|
2325
|
+
and str(failure.get("message") or "").strip()
|
|
2326
|
+
],
|
|
2327
|
+
*[
|
|
2328
|
+
str(failure.get("message") or "").strip()
|
|
2329
|
+
for failure in idea_acceptance_failures
|
|
2330
|
+
if any("seam" in str(reason or "").lower() for reason in list(failure.get("reasons") or []))
|
|
2331
|
+
and str(failure.get("message") or "").strip()
|
|
2332
|
+
],
|
|
2333
|
+
})
|
|
2334
|
+
return {
|
|
2335
|
+
"verdict_map": verdict_map,
|
|
2336
|
+
"finding_set": finding_set,
|
|
2337
|
+
"missing_seams": missing_seams,
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
def _build_validate_repair_protected_context(
|
|
2342
|
+
*,
|
|
2343
|
+
workflows: list[dict[str, Any]],
|
|
2344
|
+
failing_workflow_ids: set[str],
|
|
2345
|
+
idea_acceptance_coverage: dict[str, Any] | None = None,
|
|
2346
|
+
failing_idea_criterion_indices: set[int] | None = None,
|
|
2347
|
+
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
2348
|
+
protected_sections: list[dict[str, Any]] = []
|
|
2349
|
+
do_not_touch: list[str] = []
|
|
2350
|
+
failing_idea_criterion_indices = set(failing_idea_criterion_indices or set())
|
|
2351
|
+
for workflow in workflows:
|
|
2352
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
2353
|
+
if not workflow_id:
|
|
2354
|
+
continue
|
|
2355
|
+
side_effect_id = str(workflow.get("side_effect", {}).get("id") or "")
|
|
2356
|
+
if workflow_id not in failing_workflow_ids:
|
|
2357
|
+
protected_sections.append(
|
|
2358
|
+
{
|
|
2359
|
+
"section_type": "workflow",
|
|
2360
|
+
"workflow_id": workflow_id,
|
|
2361
|
+
"side_effect_id": side_effect_id,
|
|
2362
|
+
"reason": "workflow passed deterministic validation; preserve it",
|
|
2363
|
+
"protected_fields": [
|
|
2364
|
+
"workflow_id",
|
|
2365
|
+
"side_effect",
|
|
2366
|
+
"story_backing",
|
|
2367
|
+
"code_backing",
|
|
2368
|
+
"source_doc_backing",
|
|
2369
|
+
"implicated_users",
|
|
2370
|
+
"interaction_points",
|
|
2371
|
+
"needed_interaction_points",
|
|
2372
|
+
"process_sequence",
|
|
2373
|
+
"branches",
|
|
2374
|
+
"resulting_artifacts",
|
|
2375
|
+
],
|
|
2376
|
+
}
|
|
2377
|
+
)
|
|
2378
|
+
do_not_touch.append(f"Do not modify passing workflow {workflow_id} (side_effect={side_effect_id or 'unknown'}).")
|
|
2379
|
+
continue
|
|
2380
|
+
do_not_touch.extend(
|
|
2381
|
+
[
|
|
2382
|
+
f"Within failing workflow {workflow_id}, preserve workflow_id and side_effect anchoring.",
|
|
2383
|
+
f"Within failing workflow {workflow_id}, preserve already-supported story_backing/code_backing/source_doc_backing unless the repair source explicitly says they are wrong.",
|
|
2384
|
+
]
|
|
2385
|
+
)
|
|
2386
|
+
coverage_entries = list((idea_acceptance_coverage or {}).get("criteria") or [])
|
|
2387
|
+
for entry in coverage_entries:
|
|
2388
|
+
criterion_index = int(entry.get("criterion_index") or 0)
|
|
2389
|
+
criterion = str(entry.get("criterion") or "").strip()
|
|
2390
|
+
if criterion_index and criterion_index not in failing_idea_criterion_indices:
|
|
2391
|
+
protected_sections.append(
|
|
2392
|
+
{
|
|
2393
|
+
"section_type": "idea_acceptance_criterion",
|
|
2394
|
+
"criterion_index": criterion_index,
|
|
2395
|
+
"criterion": criterion,
|
|
2396
|
+
"reason": "idea acceptance criterion already proven; preserve its proof mapping",
|
|
2397
|
+
"protected_fields": [
|
|
2398
|
+
"criterion_index",
|
|
2399
|
+
"criterion",
|
|
2400
|
+
"verdict",
|
|
2401
|
+
"story_ids",
|
|
2402
|
+
"workflow_ids",
|
|
2403
|
+
"side_effect_ids",
|
|
2404
|
+
"evidence_refs",
|
|
2405
|
+
"proof_summary",
|
|
2406
|
+
],
|
|
2407
|
+
}
|
|
2408
|
+
)
|
|
2409
|
+
do_not_touch.append(f"Do not modify passing idea acceptance criterion #{criterion_index}: {criterion}")
|
|
2410
|
+
do_not_touch.append(
|
|
2411
|
+
f"Do not revisit already-stable idea acceptance criterion #{criterion_index} unless you can cite a contradiction in the current artifact."
|
|
2412
|
+
)
|
|
2413
|
+
elif criterion_index:
|
|
2414
|
+
do_not_touch.append(
|
|
2415
|
+
f"Within failing idea acceptance criterion #{criterion_index}, preserve criterion_index and criterion text exactly; repair only missing proof, seams, or evidence mapping."
|
|
2416
|
+
)
|
|
2417
|
+
return protected_sections, do_not_touch
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
def _validate_workflow_repair_sources(
|
|
2421
|
+
*,
|
|
2422
|
+
deterministic_errors: list[str],
|
|
2423
|
+
blocking_agentic_findings: list[dict[str, Any]],
|
|
2424
|
+
acceptance_failures: list[dict[str, Any]] | None = None,
|
|
2425
|
+
idea_acceptance_failures: list[dict[str, Any]] | None = None,
|
|
2426
|
+
) -> list[dict[str, Any]]:
|
|
2427
|
+
repair_sources: list[dict[str, Any]] = [
|
|
2428
|
+
{
|
|
2429
|
+
"source_type": "deterministic_validation_error",
|
|
2430
|
+
"message": error,
|
|
2431
|
+
}
|
|
2432
|
+
for error in deterministic_errors
|
|
2433
|
+
]
|
|
2434
|
+
repair_sources.extend(
|
|
2435
|
+
{
|
|
2436
|
+
"source_type": "agentic_blocking_finding",
|
|
2437
|
+
"workflow_id": finding.get("workflow_id"),
|
|
2438
|
+
"severity": finding.get("severity"),
|
|
2439
|
+
"summary": finding.get("summary"),
|
|
2440
|
+
"details": list(finding.get("details") or []),
|
|
2441
|
+
"blocking": bool(finding.get("blocking")),
|
|
2442
|
+
}
|
|
2443
|
+
for finding in blocking_agentic_findings
|
|
2444
|
+
)
|
|
2445
|
+
repair_sources.extend(list(acceptance_failures or []))
|
|
2446
|
+
repair_sources.extend(list(idea_acceptance_failures or []))
|
|
2447
|
+
return repair_sources
|
|
2448
|
+
|
|
2449
|
+
|
|
2450
|
+
def _interaction_point_seam_family(point: dict[str, Any]) -> str:
|
|
2451
|
+
kind = str(point.get("kind") or "").strip().lower()
|
|
2452
|
+
if not kind:
|
|
2453
|
+
kind = _normalize_interaction_type(str(point.get("type") or "").strip().lower())
|
|
2454
|
+
return kind or "no_seam"
|
|
2455
|
+
|
|
2456
|
+
|
|
2457
|
+
def _workflow_seam_family(workflow: dict[str, Any]) -> str:
|
|
2458
|
+
for point in list(workflow.get("interaction_points") or []) + list(workflow.get("needed_interaction_points") or []):
|
|
2459
|
+
family = _interaction_point_seam_family(point)
|
|
2460
|
+
if family and family != "no_seam":
|
|
2461
|
+
return family
|
|
2462
|
+
return "missing_seam"
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
def _merge_repaired_workflows(
|
|
2466
|
+
*,
|
|
2467
|
+
base_workflows: list[dict[str, Any]],
|
|
2468
|
+
repaired_workflows: list[dict[str, Any]],
|
|
2469
|
+
allowed_workflow_ids: set[str],
|
|
2470
|
+
side_effects_artifact: dict[str, Any],
|
|
2471
|
+
) -> list[dict[str, Any]]:
|
|
2472
|
+
repaired_map = {
|
|
2473
|
+
str(item.get("workflow_id") or ""): dict(item)
|
|
2474
|
+
for item in repaired_workflows
|
|
2475
|
+
if str(item.get("workflow_id") or "")
|
|
2476
|
+
}
|
|
2477
|
+
merged: list[dict[str, Any]] = []
|
|
2478
|
+
seen: set[str] = set()
|
|
2479
|
+
for workflow in base_workflows:
|
|
2480
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
2481
|
+
if workflow_id in allowed_workflow_ids and workflow_id in repaired_map:
|
|
2482
|
+
merged.append(_ground_workflow_in_side_effect(workflow=repaired_map[workflow_id], side_effects_artifact=side_effects_artifact))
|
|
2483
|
+
seen.add(workflow_id)
|
|
2484
|
+
else:
|
|
2485
|
+
merged.append(dict(workflow))
|
|
2486
|
+
for workflow_id, workflow in repaired_map.items():
|
|
2487
|
+
if workflow_id in allowed_workflow_ids and workflow_id not in seen:
|
|
2488
|
+
merged.append(_ground_workflow_in_side_effect(workflow=workflow, side_effects_artifact=side_effects_artifact))
|
|
2489
|
+
return merged
|
|
2490
|
+
|
|
2491
|
+
|
|
2492
|
+
def _merge_repaired_idea_acceptance_coverage(
|
|
2493
|
+
*,
|
|
2494
|
+
base_coverage: dict[str, Any],
|
|
2495
|
+
repaired_coverage: dict[str, Any],
|
|
2496
|
+
allowed_criterion_indices: set[int],
|
|
2497
|
+
canonical_criteria: list[str],
|
|
2498
|
+
) -> dict[str, Any]:
|
|
2499
|
+
merged = dict(base_coverage or {})
|
|
2500
|
+
base_entries = {
|
|
2501
|
+
int(entry.get("criterion_index") or 0): dict(entry)
|
|
2502
|
+
for entry in (merged.get("criteria") or [])
|
|
2503
|
+
if int(entry.get("criterion_index") or 0) > 0
|
|
2504
|
+
}
|
|
2505
|
+
repaired_entries = {
|
|
2506
|
+
int(entry.get("criterion_index") or 0): dict(entry)
|
|
2507
|
+
for entry in (repaired_coverage.get("criteria") or [])
|
|
2508
|
+
if int(entry.get("criterion_index") or 0) > 0
|
|
2509
|
+
}
|
|
2510
|
+
for idx in allowed_criterion_indices:
|
|
2511
|
+
if idx in repaired_entries:
|
|
2512
|
+
base_entries[idx] = repaired_entries[idx]
|
|
2513
|
+
merged["criteria"] = [base_entries[idx] for idx in sorted(base_entries)]
|
|
2514
|
+
repaired_lineage = repaired_coverage.get("execution_lineage")
|
|
2515
|
+
if repaired_lineage:
|
|
2516
|
+
merged["execution_lineage"] = repaired_lineage
|
|
2517
|
+
return _normalize_idea_acceptance_coverage(
|
|
2518
|
+
idea_acceptance_coverage=merged,
|
|
2519
|
+
canonical_criteria=canonical_criteria,
|
|
2520
|
+
)
|
|
2521
|
+
|
|
2522
|
+
|
|
2523
|
+
def _build_validate_repair_clusters(
|
|
2524
|
+
*,
|
|
2525
|
+
workflows: list[dict[str, Any]],
|
|
2526
|
+
repair_sources: list[dict[str, Any]],
|
|
2527
|
+
blocking_agentic_findings: list[dict[str, Any]],
|
|
2528
|
+
idea_acceptance_coverage: dict[str, Any] | None = None,
|
|
2529
|
+
failing_workflow_ids: set[str],
|
|
2530
|
+
failing_idea_criterion_indices: set[int],
|
|
2531
|
+
) -> list[dict[str, Any]]:
|
|
2532
|
+
workflow_map = {
|
|
2533
|
+
str(workflow.get("workflow_id") or ""): dict(workflow)
|
|
2534
|
+
for workflow in workflows
|
|
2535
|
+
if str(workflow.get("workflow_id") or "")
|
|
2536
|
+
}
|
|
2537
|
+
coverage_by_index = {
|
|
2538
|
+
int(entry.get("criterion_index") or 0): dict(entry)
|
|
2539
|
+
for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
|
|
2540
|
+
if int(entry.get("criterion_index") or 0) > 0
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
def cluster_key_for(*, seam_family: str, workflow_ids: set[str], criterion_indices: set[int]) -> tuple[str, str]:
|
|
2544
|
+
side_effect_ids = sorted({
|
|
2545
|
+
str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
|
|
2546
|
+
for workflow_id in workflow_ids
|
|
2547
|
+
if str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
|
|
2548
|
+
})
|
|
2549
|
+
if not side_effect_ids and criterion_indices:
|
|
2550
|
+
for idx in criterion_indices:
|
|
2551
|
+
entry = coverage_by_index.get(idx) or {}
|
|
2552
|
+
side_effect_ids.extend(
|
|
2553
|
+
sid for sid in (str(item).strip() for item in (entry.get("side_effect_ids") or [])) if sid
|
|
2554
|
+
)
|
|
2555
|
+
scope = "__".join(sorted(dict.fromkeys(side_effect_ids))) or "global"
|
|
2556
|
+
return seam_family or "global", scope
|
|
2557
|
+
|
|
2558
|
+
clusters: dict[tuple[str, str], dict[str, Any]] = {}
|
|
2559
|
+
|
|
2560
|
+
def ensure_cluster(*, seam_family: str, workflow_ids: set[str], criterion_indices: set[int]) -> dict[str, Any]:
|
|
2561
|
+
family, scope = cluster_key_for(seam_family=seam_family, workflow_ids=workflow_ids, criterion_indices=criterion_indices)
|
|
2562
|
+
key = (family, scope)
|
|
2563
|
+
cluster = clusters.get(key)
|
|
2564
|
+
if cluster is None:
|
|
2565
|
+
cluster = {
|
|
2566
|
+
"cluster_id": f"{family}__{scope}",
|
|
2567
|
+
"cluster_type": "repair_cluster",
|
|
2568
|
+
"seam_family": family,
|
|
2569
|
+
"scope": scope,
|
|
2570
|
+
"workflow_ids": set(),
|
|
2571
|
+
"side_effect_ids": set(),
|
|
2572
|
+
"criterion_indices": set(),
|
|
2573
|
+
"repair_sources": [],
|
|
2574
|
+
"blocking_findings": [],
|
|
2575
|
+
"source_types": set(),
|
|
2576
|
+
}
|
|
2577
|
+
clusters[key] = cluster
|
|
2578
|
+
cluster["workflow_ids"].update(workflow_ids)
|
|
2579
|
+
cluster["criterion_indices"].update(criterion_indices)
|
|
2580
|
+
for workflow_id in workflow_ids:
|
|
2581
|
+
side_effect_id = str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
|
|
2582
|
+
if side_effect_id:
|
|
2583
|
+
cluster["side_effect_ids"].add(side_effect_id)
|
|
2584
|
+
return cluster
|
|
2585
|
+
|
|
2586
|
+
for source in repair_sources:
|
|
2587
|
+
source_workflow_ids = {
|
|
2588
|
+
str(item).strip()
|
|
2589
|
+
for item in (source.get("workflow_ids") or [])
|
|
2590
|
+
if str(item).strip()
|
|
2591
|
+
}
|
|
2592
|
+
source_workflow_ids.update(
|
|
2593
|
+
workflow_id
|
|
2594
|
+
for workflow_id in failing_workflow_ids
|
|
2595
|
+
if workflow_id and workflow_id in str(source.get("message") or "")
|
|
2596
|
+
)
|
|
2597
|
+
source_workflow_ids.update(
|
|
2598
|
+
workflow_id
|
|
2599
|
+
for workflow_id in failing_workflow_ids
|
|
2600
|
+
if workflow_id and workflow_id == str(source.get("workflow_id") or "").strip()
|
|
2601
|
+
)
|
|
2602
|
+
criterion_index = int(source.get("criterion_index") or 0)
|
|
2603
|
+
if criterion_index <= 0:
|
|
2604
|
+
match = re.search(r"\[#(\d+)\]", str(source.get("message") or ""))
|
|
2605
|
+
if match:
|
|
2606
|
+
criterion_index = int(match.group(1))
|
|
2607
|
+
criterion_indices = {criterion_index} if criterion_index > 0 else set()
|
|
2608
|
+
if not source_workflow_ids and criterion_indices:
|
|
2609
|
+
for idx in criterion_indices:
|
|
2610
|
+
entry = coverage_by_index.get(idx) or {}
|
|
2611
|
+
source_workflow_ids.update(
|
|
2612
|
+
str(item).strip()
|
|
2613
|
+
for item in (entry.get("workflow_ids") or [])
|
|
2614
|
+
if str(item).strip()
|
|
2615
|
+
)
|
|
2616
|
+
if str(source.get("source_type") or "") == "deterministic_validation_error" and not source_workflow_ids and not criterion_indices:
|
|
2617
|
+
continue
|
|
2618
|
+
seam_families = {
|
|
2619
|
+
_workflow_seam_family(workflow_map[workflow_id])
|
|
2620
|
+
for workflow_id in source_workflow_ids
|
|
2621
|
+
if workflow_id in workflow_map
|
|
2622
|
+
}
|
|
2623
|
+
if source.get("source_type") in {"acceptance_criterion_missing_seam", "idea_acceptance_criterion_under_proven"} and not seam_families:
|
|
2624
|
+
seam_families.add("missing_seam")
|
|
2625
|
+
if source.get("source_type") == "idea_acceptance_builder_not_model_backed":
|
|
2626
|
+
seam_families.add("builder_lineage")
|
|
2627
|
+
if not seam_families:
|
|
2628
|
+
seam_families = {"global"}
|
|
2629
|
+
for seam_family in seam_families:
|
|
2630
|
+
cluster = ensure_cluster(seam_family=seam_family, workflow_ids=source_workflow_ids, criterion_indices=criterion_indices)
|
|
2631
|
+
cluster["repair_sources"].append(dict(source))
|
|
2632
|
+
source_type = str(source.get("source_type") or "deterministic_validation_error").strip()
|
|
2633
|
+
if source_type:
|
|
2634
|
+
cluster["source_types"].add(source_type)
|
|
2635
|
+
|
|
2636
|
+
for finding in blocking_agentic_findings:
|
|
2637
|
+
workflow_id = str(finding.get("workflow_id") or "").strip()
|
|
2638
|
+
workflow_ids = {workflow_id} if workflow_id else set()
|
|
2639
|
+
seam_family = _workflow_seam_family(workflow_map[workflow_id]) if workflow_id in workflow_map else "global"
|
|
2640
|
+
cluster = ensure_cluster(seam_family=seam_family, workflow_ids=workflow_ids, criterion_indices=set())
|
|
2641
|
+
cluster["blocking_findings"].append(dict(finding))
|
|
2642
|
+
cluster["source_types"].add("agentic_blocking_finding")
|
|
2643
|
+
|
|
2644
|
+
for workflow_id in sorted(failing_workflow_ids):
|
|
2645
|
+
seam_family = _workflow_seam_family(workflow_map.get(workflow_id) or {})
|
|
2646
|
+
ensure_cluster(seam_family=seam_family, workflow_ids={workflow_id}, criterion_indices=set())
|
|
2647
|
+
|
|
2648
|
+
if not clusters and (failing_workflow_ids or failing_idea_criterion_indices):
|
|
2649
|
+
cluster = ensure_cluster(seam_family="global", workflow_ids=set(failing_workflow_ids), criterion_indices=set(failing_idea_criterion_indices))
|
|
2650
|
+
cluster["source_types"].add("fallback_cluster")
|
|
2651
|
+
|
|
2652
|
+
finalized: list[dict[str, Any]] = []
|
|
2653
|
+
for cluster in clusters.values():
|
|
2654
|
+
workflow_ids = sorted(cluster["workflow_ids"])
|
|
2655
|
+
criterion_indices = sorted(cluster["criterion_indices"])
|
|
2656
|
+
if not workflow_ids and not criterion_indices and not cluster["repair_sources"] and not cluster["blocking_findings"]:
|
|
2657
|
+
continue
|
|
2658
|
+
finalized.append({
|
|
2659
|
+
"cluster_id": cluster["cluster_id"],
|
|
2660
|
+
"cluster_type": cluster["cluster_type"],
|
|
2661
|
+
"seam_family": cluster["seam_family"],
|
|
2662
|
+
"scope": cluster["scope"],
|
|
2663
|
+
"workflow_ids": workflow_ids,
|
|
2664
|
+
"side_effect_ids": sorted(cluster["side_effect_ids"]),
|
|
2665
|
+
"criterion_indices": criterion_indices,
|
|
2666
|
+
"repair_sources": cluster["repair_sources"],
|
|
2667
|
+
"blocking_findings": cluster["blocking_findings"],
|
|
2668
|
+
"source_types": sorted(cluster["source_types"]),
|
|
2669
|
+
"failing_workflows": [workflow_map[workflow_id] for workflow_id in workflow_ids if workflow_id in workflow_map],
|
|
2670
|
+
"failing_idea_acceptance_criteria": [coverage_by_index[idx] for idx in criterion_indices if idx in coverage_by_index],
|
|
2671
|
+
})
|
|
2672
|
+
finalized.sort(key=lambda item: (item["seam_family"], item["scope"], item["cluster_id"]))
|
|
2673
|
+
return finalized
|
|
2674
|
+
|
|
2675
|
+
|
|
2676
|
+
def _persist_validate_repair_clusters(
|
|
2677
|
+
*,
|
|
2678
|
+
repo_root: Path | None,
|
|
2679
|
+
idea_id: str | None,
|
|
2680
|
+
pipeline_dir: Path | None,
|
|
2681
|
+
payload: dict[str, Any],
|
|
2682
|
+
) -> None:
|
|
2683
|
+
if repo_root is None or idea_id is None or pipeline_dir is None:
|
|
2684
|
+
return
|
|
2685
|
+
_write_integration_artifact(
|
|
2686
|
+
repo_root=repo_root,
|
|
2687
|
+
idea_id=idea_id,
|
|
2688
|
+
pipeline_dir=pipeline_dir,
|
|
2689
|
+
filename="validation_repair_clusters.json",
|
|
2690
|
+
data=payload,
|
|
2691
|
+
)
|
|
2692
|
+
|
|
2693
|
+
|
|
2694
|
+
def _validate_workflows(
|
|
2695
|
+
*,
|
|
2696
|
+
side_effects_artifact: dict[str, Any],
|
|
2697
|
+
implicated_users_artifact: dict[str, Any],
|
|
2698
|
+
workflows: list[dict[str, Any]],
|
|
2699
|
+
implemented_idea: dict[str, Any] | None = None,
|
|
2700
|
+
implemented_stories: list[dict[str, Any]] | None = None,
|
|
2701
|
+
idea_acceptance_coverage: dict[str, Any] | None = None,
|
|
2702
|
+
max_iterations: int = 4,
|
|
2703
|
+
agentic_review: dict[str, Any] | None = None,
|
|
2704
|
+
repo_root: Path | None = None,
|
|
2705
|
+
idea_id: str | None = None,
|
|
2706
|
+
pipeline_dir: Path | None = None,
|
|
2707
|
+
repair_node_id_prefix: str = "validate",
|
|
2708
|
+
code_evidence: list[dict[str, Any]] | None = None,
|
|
2709
|
+
source_docs: list[dict[str, Any]] | None = None,
|
|
2710
|
+
) -> tuple[dict[str, Any], int]:
|
|
2711
|
+
iterations_used = 0
|
|
2712
|
+
validations: list[dict[str, Any]] = []
|
|
2713
|
+
pending = [dict(workflow) for workflow in workflows]
|
|
2714
|
+
expected_side_effect_ids = {str(item.get("id") or "") for item in side_effects_artifact.get("side_effects") or [] if str(item.get("id") or "")}
|
|
2715
|
+
expected_users = {
|
|
2716
|
+
(str(user.get("kind") or ""), str(user.get("scope") or ""), str(user.get("authority") or ""))
|
|
2717
|
+
for user in implicated_users_artifact.get("implicated_users") or []
|
|
2718
|
+
}
|
|
2719
|
+
implemented_stories = list(implemented_stories or [])
|
|
2720
|
+
story_acceptance = _story_acceptance_index(implemented_stories)
|
|
2721
|
+
story_index = {
|
|
2722
|
+
str(story.get("story_id") or story.get("id") or "").strip(): dict(story)
|
|
2723
|
+
for story in implemented_stories
|
|
2724
|
+
if str(story.get("story_id") or story.get("id") or "").strip()
|
|
2725
|
+
}
|
|
2726
|
+
idea_acceptance_criteria = _idea_acceptance_criteria(implemented_idea)
|
|
2727
|
+
agentic_review = dict(agentic_review or {})
|
|
2728
|
+
pending_idea_acceptance_coverage = _normalize_idea_acceptance_coverage(
|
|
2729
|
+
idea_acceptance_coverage=idea_acceptance_coverage,
|
|
2730
|
+
canonical_criteria=idea_acceptance_criteria,
|
|
2731
|
+
)
|
|
2732
|
+
agentic_findings = list(agentic_review.get("findings") or [])
|
|
2733
|
+
blocking_agentic_findings = [
|
|
2734
|
+
finding
|
|
2735
|
+
for finding in agentic_findings
|
|
2736
|
+
if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
|
|
2737
|
+
]
|
|
2738
|
+
|
|
2739
|
+
cluster_history: list[dict[str, Any]] = []
|
|
2740
|
+
prior_convergence_state: dict[str, Any] | None = None
|
|
2741
|
+
final_errors: list[str] = []
|
|
2742
|
+
final_idea_acceptance_failures: list[dict[str, Any]] = []
|
|
2743
|
+
final_repair_clusters_payload = {
|
|
2744
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
2745
|
+
"mode": "internal_sequential_clusters",
|
|
2746
|
+
"iterations": [],
|
|
2747
|
+
"latest_iteration": None,
|
|
2748
|
+
"unresolved_cluster_ids": [],
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
for iteration in range(1, max_iterations + 1):
|
|
2752
|
+
iterations_used = iteration
|
|
2753
|
+
errors: list[str] = []
|
|
2754
|
+
acceptance_failures: list[dict[str, Any]] = []
|
|
2755
|
+
idea_acceptance_failures: list[dict[str, Any]] = []
|
|
2756
|
+
workflow_side_effect_ids = {str(item.get("side_effect", {}).get("id") or "") for item in pending}
|
|
2757
|
+
if workflow_side_effect_ids != expected_side_effect_ids:
|
|
2758
|
+
missing = sorted(expected_side_effect_ids - workflow_side_effect_ids)
|
|
2759
|
+
if missing:
|
|
2760
|
+
errors.append(f"missing workflows for side effects: {', '.join(missing)}")
|
|
2761
|
+
|
|
2762
|
+
attached_users = {
|
|
2763
|
+
(str(user.get("kind") or ""), str(user.get("scope") or ""), str(user.get("authority") or ""))
|
|
2764
|
+
for workflow in pending
|
|
2765
|
+
for user in (workflow.get("implicated_users") or [])
|
|
2766
|
+
}
|
|
2767
|
+
if not expected_users.issubset(attached_users):
|
|
2768
|
+
errors.append("all implicated users covered: false")
|
|
2769
|
+
|
|
2770
|
+
side_effect_map = _workflow_side_effect_map(side_effects_artifact)
|
|
2771
|
+
story_to_workflow_ids: dict[str, set[str]] = {}
|
|
2772
|
+
workflow_map: dict[str, dict[str, Any]] = {}
|
|
2773
|
+
for workflow in pending:
|
|
2774
|
+
workflow_id = str(workflow.get("workflow_id") or "")
|
|
2775
|
+
if workflow_id:
|
|
2776
|
+
workflow_map[workflow_id] = workflow
|
|
2777
|
+
for story_id in _workflow_story_ids(workflow):
|
|
2778
|
+
story_to_workflow_ids.setdefault(story_id, set()).add(workflow_id)
|
|
2779
|
+
for workflow in pending:
|
|
2780
|
+
wf_id = str(workflow.get("workflow_id") or "")
|
|
2781
|
+
wf_se_id = str(workflow.get("side_effect", {}).get("id") or "")
|
|
2782
|
+
expected_side_effect = side_effect_map.get(wf_se_id) or {}
|
|
2783
|
+
if wf_se_id and wf_se_id not in expected_side_effect_ids:
|
|
2784
|
+
errors.append(f"story alignment mismatch for {wf_id}")
|
|
2785
|
+
if not workflow.get("story_backing"):
|
|
2786
|
+
errors.append(f"story alignment missing for {wf_id}")
|
|
2787
|
+
if not workflow.get("code_backing"):
|
|
2788
|
+
errors.append(f"code alignment missing for {wf_id}")
|
|
2789
|
+
interaction_points = list(workflow.get("interaction_points") or [])
|
|
2790
|
+
needed_points = list(workflow.get("needed_interaction_points") or [])
|
|
2791
|
+
expected_interaction_points = list(expected_side_effect.get("interaction_points") or [])
|
|
2792
|
+
expected_needed_points = list(expected_side_effect.get("needed_interaction_points") or [])
|
|
2793
|
+
if interaction_points != expected_interaction_points or needed_points != expected_needed_points:
|
|
2794
|
+
errors.append(f"interaction seam drift for {wf_id}")
|
|
2795
|
+
if not interaction_points and not needed_points:
|
|
2796
|
+
errors.append(f"interaction points missing for {wf_id}")
|
|
2797
|
+
if workflow.get("anchor_user"):
|
|
2798
|
+
errors.append(f"workflow anchored to user instead of side effect for {wf_id}")
|
|
2799
|
+
|
|
2800
|
+
for story_id, story_contract in story_acceptance.items():
|
|
2801
|
+
related_workflow_ids = sorted(story_to_workflow_ids.get(story_id) or [])
|
|
2802
|
+
if not related_workflow_ids:
|
|
2803
|
+
for idx, criterion in enumerate(story_contract.get("acceptance_criteria") or [], start=1):
|
|
2804
|
+
message = f"acceptance criteria uncovered for {story_id} [#{idx}]: {criterion}"
|
|
2805
|
+
errors.append(message)
|
|
2806
|
+
acceptance_failures.append(
|
|
2807
|
+
{
|
|
2808
|
+
"source_type": "acceptance_criterion_uncovered",
|
|
2809
|
+
"story_id": story_id,
|
|
2810
|
+
"story_title": story_contract.get("title"),
|
|
2811
|
+
"criterion_index": idx,
|
|
2812
|
+
"criterion": criterion,
|
|
2813
|
+
"workflow_ids": [],
|
|
2814
|
+
"message": message,
|
|
2815
|
+
}
|
|
2816
|
+
)
|
|
2817
|
+
continue
|
|
2818
|
+
seam_workflow_ids = [
|
|
2819
|
+
workflow_id
|
|
2820
|
+
for workflow_id in related_workflow_ids
|
|
2821
|
+
if (workflow_map.get(workflow_id) or {}).get("interaction_points") or (workflow_map.get(workflow_id) or {}).get("needed_interaction_points")
|
|
2822
|
+
]
|
|
2823
|
+
if seam_workflow_ids:
|
|
2824
|
+
continue
|
|
2825
|
+
for idx, criterion in enumerate(story_contract.get("acceptance_criteria") or [], start=1):
|
|
2826
|
+
message = f"acceptance criterion missing seam for {story_id} [#{idx}]: {criterion}"
|
|
2827
|
+
errors.append(message)
|
|
2828
|
+
acceptance_failures.append(
|
|
2829
|
+
{
|
|
2830
|
+
"source_type": "acceptance_criterion_missing_seam",
|
|
2831
|
+
"story_id": story_id,
|
|
2832
|
+
"story_title": story_contract.get("title"),
|
|
2833
|
+
"criterion_index": idx,
|
|
2834
|
+
"criterion": criterion,
|
|
2835
|
+
"workflow_ids": related_workflow_ids,
|
|
2836
|
+
"message": message,
|
|
2837
|
+
}
|
|
2838
|
+
)
|
|
2839
|
+
|
|
2840
|
+
coverage_entries = list(pending_idea_acceptance_coverage.get("criteria") or [])
|
|
2841
|
+
coverage_by_index = {
|
|
2842
|
+
int(entry.get("criterion_index") or 0): dict(entry)
|
|
2843
|
+
for entry in coverage_entries
|
|
2844
|
+
if int(entry.get("criterion_index") or 0) > 0
|
|
2845
|
+
}
|
|
2846
|
+
for idx, criterion in enumerate(idea_acceptance_criteria, start=1):
|
|
2847
|
+
entry = coverage_by_index.get(idx)
|
|
2848
|
+
if entry is None:
|
|
2849
|
+
message = f"idea acceptance criterion uncovered [#{idx}]: {criterion}"
|
|
2850
|
+
errors.append(message)
|
|
2851
|
+
idea_acceptance_failures.append(
|
|
2852
|
+
{
|
|
2853
|
+
"source_type": "idea_acceptance_criterion_uncovered",
|
|
2854
|
+
"criterion_index": idx,
|
|
2855
|
+
"criterion": criterion,
|
|
2856
|
+
"workflow_ids": [],
|
|
2857
|
+
"story_ids": [],
|
|
2858
|
+
"message": message,
|
|
2859
|
+
}
|
|
2860
|
+
)
|
|
2861
|
+
continue
|
|
2862
|
+
if str(entry.get("criterion") or "").strip() != criterion:
|
|
2863
|
+
message = f"idea acceptance criterion drift [#{idx}]: expected '{criterion}'"
|
|
2864
|
+
errors.append(message)
|
|
2865
|
+
idea_acceptance_failures.append(
|
|
2866
|
+
{
|
|
2867
|
+
"source_type": "idea_acceptance_criterion_drift",
|
|
2868
|
+
"criterion_index": idx,
|
|
2869
|
+
"criterion": criterion,
|
|
2870
|
+
"workflow_ids": list(entry.get("workflow_ids") or []),
|
|
2871
|
+
"story_ids": list(entry.get("story_ids") or []),
|
|
2872
|
+
"message": message,
|
|
2873
|
+
}
|
|
2874
|
+
)
|
|
2875
|
+
verdict = str(entry.get("verdict") or "").strip().lower()
|
|
2876
|
+
story_ids = [sid for sid in (str(item).strip() for item in (entry.get("story_ids") or [])) if sid]
|
|
2877
|
+
workflow_ids = [wid for wid in (str(item).strip() for item in (entry.get("workflow_ids") or [])) if wid]
|
|
2878
|
+
evidence_refs = [ref for ref in (str(item).strip() for item in (entry.get("evidence_refs") or [])) if ref]
|
|
2879
|
+
mapped_story_ids = [sid for sid in story_ids if sid in story_index]
|
|
2880
|
+
mapped_workflow_ids = [wid for wid in workflow_ids if wid in workflow_map]
|
|
2881
|
+
seam_workflow_ids = [
|
|
2882
|
+
wid for wid in mapped_workflow_ids
|
|
2883
|
+
if (workflow_map.get(wid) or {}).get("interaction_points") or (workflow_map.get(wid) or {}).get("needed_interaction_points")
|
|
2884
|
+
]
|
|
2885
|
+
under_proven_reasons: list[str] = []
|
|
2886
|
+
if verdict != "covered":
|
|
2887
|
+
under_proven_reasons.append(f"verdict={verdict or 'missing'}")
|
|
2888
|
+
if not mapped_story_ids:
|
|
2889
|
+
under_proven_reasons.append("missing story mapping")
|
|
2890
|
+
if not mapped_workflow_ids:
|
|
2891
|
+
under_proven_reasons.append("missing workflow mapping")
|
|
2892
|
+
if mapped_workflow_ids and not seam_workflow_ids:
|
|
2893
|
+
under_proven_reasons.append("workflow mapping has no interaction seam")
|
|
2894
|
+
if not evidence_refs:
|
|
2895
|
+
under_proven_reasons.append("missing evidence refs")
|
|
2896
|
+
if not str(entry.get("proof_summary") or "").strip():
|
|
2897
|
+
under_proven_reasons.append("missing proof summary")
|
|
2898
|
+
if under_proven_reasons:
|
|
2899
|
+
message = f"idea acceptance criterion under-proven [#{idx}]: {criterion} :: {', '.join(under_proven_reasons)}"
|
|
2900
|
+
errors.append(message)
|
|
2901
|
+
idea_acceptance_failures.append(
|
|
2902
|
+
{
|
|
2903
|
+
"source_type": "idea_acceptance_criterion_under_proven",
|
|
2904
|
+
"criterion_index": idx,
|
|
2905
|
+
"criterion": criterion,
|
|
2906
|
+
"workflow_ids": mapped_workflow_ids,
|
|
2907
|
+
"story_ids": mapped_story_ids,
|
|
2908
|
+
"message": message,
|
|
2909
|
+
"reasons": under_proven_reasons,
|
|
2910
|
+
}
|
|
2911
|
+
)
|
|
2912
|
+
|
|
2913
|
+
lineage = pending_idea_acceptance_coverage.get("execution_lineage") if isinstance(pending_idea_acceptance_coverage, dict) else None
|
|
2914
|
+
if idea_acceptance_criteria:
|
|
2915
|
+
if not _idea_acceptance_lineage_is_model_backed(lineage):
|
|
2916
|
+
errors.append("idea acceptance builder lineage missing model-backed proof")
|
|
2917
|
+
idea_acceptance_failures.append(
|
|
2918
|
+
{
|
|
2919
|
+
"source_type": "idea_acceptance_builder_not_model_backed",
|
|
2920
|
+
"criterion_index": None,
|
|
2921
|
+
"criterion": "",
|
|
2922
|
+
"workflow_ids": [],
|
|
2923
|
+
"story_ids": [],
|
|
2924
|
+
"message": "idea acceptance builder lineage missing model-backed proof",
|
|
2925
|
+
}
|
|
2926
|
+
)
|
|
2927
|
+
|
|
2928
|
+
if not attached_users.issubset(expected_users) and expected_users:
|
|
2929
|
+
errors.append("invalid actors admitted into workflows")
|
|
2930
|
+
|
|
2931
|
+
deterministic_errors = list(errors)
|
|
2932
|
+
errors.extend(
|
|
2933
|
+
f"agentic review blocking: {finding.get('workflow_id') or 'global'} :: {finding.get('summary') or 'unspecified issue'}"
|
|
2934
|
+
for finding in blocking_agentic_findings
|
|
2935
|
+
)
|
|
2936
|
+
repair_sources = _validate_workflow_repair_sources(
|
|
2937
|
+
deterministic_errors=deterministic_errors,
|
|
2938
|
+
blocking_agentic_findings=blocking_agentic_findings,
|
|
2939
|
+
acceptance_failures=acceptance_failures,
|
|
2940
|
+
idea_acceptance_failures=idea_acceptance_failures,
|
|
2941
|
+
)
|
|
2942
|
+
failing_workflow_ids = _determine_failing_workflow_ids(
|
|
2943
|
+
workflows=pending,
|
|
2944
|
+
errors=deterministic_errors,
|
|
2945
|
+
blocking_agentic_findings=blocking_agentic_findings,
|
|
2946
|
+
repair_sources=repair_sources,
|
|
2947
|
+
)
|
|
2948
|
+
failing_idea_criterion_indices = {
|
|
2949
|
+
int(item.get("criterion_index") or 0)
|
|
2950
|
+
for item in idea_acceptance_failures
|
|
2951
|
+
if int(item.get("criterion_index") or 0) > 0
|
|
2952
|
+
}
|
|
2953
|
+
protected_sections, do_not_touch = _build_validate_repair_protected_context(
|
|
2954
|
+
workflows=pending,
|
|
2955
|
+
failing_workflow_ids=failing_workflow_ids,
|
|
2956
|
+
idea_acceptance_coverage=pending_idea_acceptance_coverage,
|
|
2957
|
+
failing_idea_criterion_indices=failing_idea_criterion_indices,
|
|
2958
|
+
)
|
|
2959
|
+
repair_clusters = _build_validate_repair_clusters(
|
|
2960
|
+
workflows=pending,
|
|
2961
|
+
repair_sources=repair_sources,
|
|
2962
|
+
blocking_agentic_findings=blocking_agentic_findings,
|
|
2963
|
+
idea_acceptance_coverage=pending_idea_acceptance_coverage,
|
|
2964
|
+
failing_workflow_ids=failing_workflow_ids,
|
|
2965
|
+
failing_idea_criterion_indices=failing_idea_criterion_indices,
|
|
2966
|
+
)
|
|
2967
|
+
|
|
2968
|
+
iteration_record = {
|
|
2969
|
+
"iteration": iteration,
|
|
2970
|
+
"deterministic_errors": deterministic_errors,
|
|
2971
|
+
"blocking_agentic_findings": blocking_agentic_findings,
|
|
2972
|
+
"acceptance_failures": acceptance_failures,
|
|
2973
|
+
"idea_acceptance_failures": idea_acceptance_failures,
|
|
2974
|
+
"repair_sources": repair_sources,
|
|
2975
|
+
"failing_workflow_ids": sorted(failing_workflow_ids),
|
|
2976
|
+
"failing_idea_criterion_indices": sorted(failing_idea_criterion_indices),
|
|
2977
|
+
"protected_sections": protected_sections,
|
|
2978
|
+
"do_not_touch": do_not_touch,
|
|
2979
|
+
"repair_clusters": [
|
|
2980
|
+
{
|
|
2981
|
+
"cluster_id": cluster["cluster_id"],
|
|
2982
|
+
"cluster_type": cluster["cluster_type"],
|
|
2983
|
+
"seam_family": cluster["seam_family"],
|
|
2984
|
+
"scope": cluster["scope"],
|
|
2985
|
+
"workflow_ids": list(cluster["workflow_ids"]),
|
|
2986
|
+
"side_effect_ids": list(cluster["side_effect_ids"]),
|
|
2987
|
+
"criterion_indices": list(cluster["criterion_indices"]),
|
|
2988
|
+
"source_types": list(cluster["source_types"]),
|
|
2989
|
+
}
|
|
2990
|
+
for cluster in repair_clusters
|
|
2991
|
+
],
|
|
2992
|
+
"errors": list(errors),
|
|
2993
|
+
}
|
|
2994
|
+
convergence_state = _validation_convergence_state(
|
|
2995
|
+
idea_acceptance_coverage=pending_idea_acceptance_coverage,
|
|
2996
|
+
deterministic_errors=deterministic_errors,
|
|
2997
|
+
blocking_agentic_findings=blocking_agentic_findings,
|
|
2998
|
+
acceptance_failures=acceptance_failures,
|
|
2999
|
+
idea_acceptance_failures=idea_acceptance_failures,
|
|
3000
|
+
)
|
|
3001
|
+
iteration_record["convergence_state"] = convergence_state
|
|
3002
|
+
validations.append(iteration_record)
|
|
3003
|
+
cluster_iteration_record = {
|
|
3004
|
+
"iteration": iteration,
|
|
3005
|
+
"cluster_execution_mode": "internal_sequential",
|
|
3006
|
+
"clusters": [],
|
|
3007
|
+
}
|
|
3008
|
+
final_repair_clusters_payload = {
|
|
3009
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
3010
|
+
"mode": "internal_sequential_clusters",
|
|
3011
|
+
"iterations": cluster_history + [cluster_iteration_record],
|
|
3012
|
+
"latest_iteration": iteration,
|
|
3013
|
+
"unresolved_cluster_ids": [cluster["cluster_id"] for cluster in repair_clusters],
|
|
3014
|
+
}
|
|
3015
|
+
_persist_validate_repair_clusters(
|
|
3016
|
+
repo_root=repo_root,
|
|
3017
|
+
idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
|
|
3018
|
+
pipeline_dir=pipeline_dir,
|
|
3019
|
+
payload=final_repair_clusters_payload,
|
|
3020
|
+
)
|
|
3021
|
+
|
|
3022
|
+
final_errors = errors
|
|
3023
|
+
final_idea_acceptance_failures = idea_acceptance_failures
|
|
3024
|
+
if not errors:
|
|
3025
|
+
iteration_record["stop_reason"] = "passed"
|
|
3026
|
+
cluster_history.append(cluster_iteration_record)
|
|
3027
|
+
final_repair_clusters_payload = {
|
|
3028
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
3029
|
+
"mode": "internal_sequential_clusters",
|
|
3030
|
+
"iterations": cluster_history,
|
|
3031
|
+
"latest_iteration": iteration,
|
|
3032
|
+
"unresolved_cluster_ids": [],
|
|
3033
|
+
}
|
|
3034
|
+
_persist_validate_repair_clusters(
|
|
3035
|
+
repo_root=repo_root,
|
|
3036
|
+
idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
|
|
3037
|
+
pipeline_dir=pipeline_dir,
|
|
3038
|
+
payload=final_repair_clusters_payload,
|
|
3039
|
+
)
|
|
3040
|
+
break
|
|
3041
|
+
|
|
3042
|
+
if prior_convergence_state == convergence_state:
|
|
3043
|
+
iteration_record["stop_reason"] = "converged_without_delta"
|
|
3044
|
+
cluster_history.append(cluster_iteration_record)
|
|
3045
|
+
final_repair_clusters_payload = {
|
|
3046
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
3047
|
+
"mode": "internal_sequential_clusters",
|
|
3048
|
+
"iterations": cluster_history,
|
|
3049
|
+
"latest_iteration": iteration,
|
|
3050
|
+
"unresolved_cluster_ids": [cluster["cluster_id"] for cluster in repair_clusters],
|
|
3051
|
+
}
|
|
3052
|
+
_persist_validate_repair_clusters(
|
|
3053
|
+
repo_root=repo_root,
|
|
3054
|
+
idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
|
|
3055
|
+
pipeline_dir=pipeline_dir,
|
|
3056
|
+
payload=final_repair_clusters_payload,
|
|
3057
|
+
)
|
|
3058
|
+
break
|
|
3059
|
+
|
|
3060
|
+
prior_convergence_state = convergence_state
|
|
3061
|
+
|
|
3062
|
+
if iteration < max_iterations and repo_root is not None and idea_id is not None:
|
|
3063
|
+
blocking_for_repair = [
|
|
3064
|
+
f for f in list(agentic_review.get("findings") or [])
|
|
3065
|
+
if bool(f.get("blocking")) or str(f.get("severity") or "").lower() in {"error", "blocking", "critical"}
|
|
3066
|
+
]
|
|
3067
|
+
for cluster_index, cluster in enumerate(repair_clusters, start=1):
|
|
3068
|
+
cluster_workflow_ids = {str(item).strip() for item in (cluster.get("workflow_ids") or []) if str(item).strip()}
|
|
3069
|
+
cluster_criterion_indices = {int(item) for item in (cluster.get("criterion_indices") or []) if int(item) > 0}
|
|
3070
|
+
scoped_workflows = [
|
|
3071
|
+
dict(workflow)
|
|
3072
|
+
for workflow in pending
|
|
3073
|
+
if str(workflow.get("workflow_id") or "") in cluster_workflow_ids
|
|
3074
|
+
]
|
|
3075
|
+
scoped_coverage = _filter_idea_acceptance_coverage(
|
|
3076
|
+
idea_acceptance_coverage=pending_idea_acceptance_coverage,
|
|
3077
|
+
criterion_indices=cluster_criterion_indices,
|
|
3078
|
+
)
|
|
3079
|
+
cluster_protected_sections, cluster_do_not_touch = _build_validate_repair_protected_context(
|
|
3080
|
+
workflows=pending,
|
|
3081
|
+
failing_workflow_ids=cluster_workflow_ids,
|
|
3082
|
+
idea_acceptance_coverage=pending_idea_acceptance_coverage,
|
|
3083
|
+
failing_idea_criterion_indices=cluster_criterion_indices,
|
|
3084
|
+
)
|
|
3085
|
+
focused_code_evidence = _load_code_evidence_for_workflows(
|
|
3086
|
+
workflows=[workflow for workflow in pending if str(workflow.get("workflow_id") or "") in cluster_workflow_ids],
|
|
3087
|
+
code_evidence=code_evidence,
|
|
3088
|
+
repo_root=repo_root,
|
|
3089
|
+
) if cluster_workflow_ids else []
|
|
3090
|
+
cluster_context = {
|
|
3091
|
+
"idea_id": idea_id,
|
|
3092
|
+
"implemented_idea": implemented_idea or {},
|
|
3093
|
+
"implemented_stories": implemented_stories,
|
|
3094
|
+
"enriched_workflows": scoped_workflows,
|
|
3095
|
+
"idea_acceptance_coverage": scoped_coverage,
|
|
3096
|
+
"repair_cluster": {
|
|
3097
|
+
"cluster_id": cluster["cluster_id"],
|
|
3098
|
+
"cluster_type": cluster["cluster_type"],
|
|
3099
|
+
"cluster_index": cluster_index,
|
|
3100
|
+
"cluster_total": len(repair_clusters),
|
|
3101
|
+
"seam_family": cluster["seam_family"],
|
|
3102
|
+
"scope": cluster["scope"],
|
|
3103
|
+
"workflow_ids": sorted(cluster_workflow_ids),
|
|
3104
|
+
"side_effect_ids": list(cluster.get("side_effect_ids") or []),
|
|
3105
|
+
"criterion_indices": sorted(cluster_criterion_indices),
|
|
3106
|
+
"repair_sources": list(cluster.get("repair_sources") or []),
|
|
3107
|
+
"blocking_findings": list(cluster.get("blocking_findings") or []),
|
|
3108
|
+
"source_types": list(cluster.get("source_types") or []),
|
|
3109
|
+
},
|
|
3110
|
+
"failing_workflows": scoped_workflows,
|
|
3111
|
+
"failing_idea_acceptance_criteria": [
|
|
3112
|
+
failure for failure in idea_acceptance_failures
|
|
3113
|
+
if int(failure.get("criterion_index") or 0) in cluster_criterion_indices
|
|
3114
|
+
],
|
|
3115
|
+
"remaining_failing_workflow_ids": sorted(cluster_workflow_ids),
|
|
3116
|
+
"remaining_failing_idea_criterion_indices": sorted(cluster_criterion_indices),
|
|
3117
|
+
"deterministic_errors": [
|
|
3118
|
+
error for error in deterministic_errors
|
|
3119
|
+
if any(workflow_id in error for workflow_id in cluster_workflow_ids)
|
|
3120
|
+
or any(f"[#{criterion_index}]" in error for criterion_index in cluster_criterion_indices)
|
|
3121
|
+
or (not cluster_workflow_ids and not cluster_criterion_indices)
|
|
3122
|
+
] or deterministic_errors,
|
|
3123
|
+
"blocking_findings": [
|
|
3124
|
+
finding for finding in blocking_for_repair
|
|
3125
|
+
if not cluster_workflow_ids or str(finding.get("workflow_id") or "").strip() in cluster_workflow_ids
|
|
3126
|
+
] or list(cluster.get("blocking_findings") or []),
|
|
3127
|
+
"repair_sources": list(cluster.get("repair_sources") or []),
|
|
3128
|
+
"protected_sections": cluster_protected_sections,
|
|
3129
|
+
"do_not_touch": cluster_do_not_touch,
|
|
3130
|
+
"focused_code_evidence": focused_code_evidence,
|
|
3131
|
+
"source_docs": list(source_docs or []),
|
|
3132
|
+
"prior_iteration_convergence_state": prior_convergence_state,
|
|
3133
|
+
"required_repair_delta_ledger": {
|
|
3134
|
+
"must_state": [
|
|
3135
|
+
"what verdicts changed",
|
|
3136
|
+
"what evidence changed",
|
|
3137
|
+
"what seam was newly resolved",
|
|
3138
|
+
],
|
|
3139
|
+
"stable_criteria_rule": "Do not revisit already-stable criteria unless you can cite a contradiction in the current artifact.",
|
|
3140
|
+
},
|
|
3141
|
+
"repair_instruction": "Repair only this internal VEG repair_cluster. Work only on remaining failing workflows and remaining failing idea-acceptance criteria. Preserve protected_sections and obey do_not_touch. Do not rewrite unrelated workflows, do not re-summarize the full artifact, and do not revisit already-stable criteria without citing a contradiction in the current artifact.",
|
|
3142
|
+
}
|
|
3143
|
+
repair_artifact, repair_envelope = integration_agentic.run_integration_agent_step(
|
|
3144
|
+
repo_root=repo_root,
|
|
3145
|
+
stage_name="validate_repair",
|
|
3146
|
+
output_model=VegIdeaAcceptanceBuilderArtifact,
|
|
3147
|
+
context_payload=cluster_context,
|
|
3148
|
+
guidance=[],
|
|
3149
|
+
timeout_seconds=_integration_agent_timeout_seconds(),
|
|
3150
|
+
)
|
|
3151
|
+
if pipeline_dir is not None:
|
|
3152
|
+
integration_agentic.persist_agent_run(
|
|
3153
|
+
pipeline_root=pipeline_dir,
|
|
3154
|
+
node_id=f"{repair_node_id_prefix}_repair_iter{iteration}_cluster{cluster_index}",
|
|
3155
|
+
envelope=repair_envelope,
|
|
3156
|
+
)
|
|
3157
|
+
pending = _merge_repaired_workflows(
|
|
3158
|
+
base_workflows=pending,
|
|
3159
|
+
repaired_workflows=[item.model_dump() for item in repair_artifact.enriched_workflows],
|
|
3160
|
+
allowed_workflow_ids=cluster_workflow_ids,
|
|
3161
|
+
side_effects_artifact=side_effects_artifact,
|
|
3162
|
+
)
|
|
3163
|
+
pending_idea_acceptance_coverage = _merge_repaired_idea_acceptance_coverage(
|
|
3164
|
+
base_coverage=pending_idea_acceptance_coverage,
|
|
3165
|
+
repaired_coverage=repair_artifact.idea_acceptance_coverage.model_dump(),
|
|
3166
|
+
allowed_criterion_indices=cluster_criterion_indices,
|
|
3167
|
+
canonical_criteria=idea_acceptance_criteria,
|
|
3168
|
+
)
|
|
3169
|
+
latest_cluster_review = repair_artifact.model_dump()
|
|
3170
|
+
cluster_iteration_record["clusters"].append(
|
|
3171
|
+
{
|
|
3172
|
+
"cluster_id": cluster["cluster_id"],
|
|
3173
|
+
"cluster_type": cluster["cluster_type"],
|
|
3174
|
+
"cluster_index": cluster_index,
|
|
3175
|
+
"seam_family": cluster["seam_family"],
|
|
3176
|
+
"scope": cluster["scope"],
|
|
3177
|
+
"workflow_ids": sorted(cluster_workflow_ids),
|
|
3178
|
+
"criterion_indices": sorted(cluster_criterion_indices),
|
|
3179
|
+
"source_types": list(cluster.get("source_types") or []),
|
|
3180
|
+
"repair_sources": list(cluster.get("repair_sources") or []),
|
|
3181
|
+
"protected_sections": cluster_protected_sections,
|
|
3182
|
+
"do_not_touch": cluster_do_not_touch,
|
|
3183
|
+
"agentic_review_summary": latest_cluster_review.get("summary"),
|
|
3184
|
+
"model_backed": _idea_acceptance_lineage_is_model_backed((repair_artifact.idea_acceptance_coverage.model_dump() or {}).get("execution_lineage")),
|
|
3185
|
+
"repair_delta_ledger": (
|
|
3186
|
+
repair_artifact.repair_delta_ledger.model_dump()
|
|
3187
|
+
if repair_artifact.repair_delta_ledger is not None
|
|
3188
|
+
else None
|
|
3189
|
+
),
|
|
3190
|
+
}
|
|
3191
|
+
)
|
|
3192
|
+
agentic_review = latest_cluster_review
|
|
3193
|
+
agentic_findings = list(agentic_review.get("findings") or [])
|
|
3194
|
+
blocking_agentic_findings = [
|
|
3195
|
+
finding
|
|
3196
|
+
for finding in agentic_findings
|
|
3197
|
+
if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
|
|
3198
|
+
]
|
|
3199
|
+
final_repair_clusters_payload = {
|
|
3200
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
3201
|
+
"mode": "internal_sequential_clusters",
|
|
3202
|
+
"iterations": cluster_history + [cluster_iteration_record],
|
|
3203
|
+
"latest_iteration": iteration,
|
|
3204
|
+
"unresolved_cluster_ids": [item["cluster_id"] for item in repair_clusters[cluster_index:]],
|
|
3205
|
+
}
|
|
3206
|
+
_persist_validate_repair_clusters(
|
|
3207
|
+
repo_root=repo_root,
|
|
3208
|
+
idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
|
|
3209
|
+
pipeline_dir=pipeline_dir,
|
|
3210
|
+
payload=final_repair_clusters_payload,
|
|
3211
|
+
)
|
|
3212
|
+
|
|
3213
|
+
cluster_history.append(cluster_iteration_record)
|
|
3214
|
+
final_repair_clusters_payload = {
|
|
3215
|
+
"idea_id": idea_id or side_effects_artifact.get("idea_id"),
|
|
3216
|
+
"mode": "internal_sequential_clusters",
|
|
3217
|
+
"iterations": cluster_history,
|
|
3218
|
+
"latest_iteration": iteration,
|
|
3219
|
+
"unresolved_cluster_ids": [],
|
|
3220
|
+
}
|
|
3221
|
+
_persist_validate_repair_clusters(
|
|
3222
|
+
repo_root=repo_root,
|
|
3223
|
+
idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
|
|
3224
|
+
pipeline_dir=pipeline_dir,
|
|
3225
|
+
payload=final_repair_clusters_payload,
|
|
3226
|
+
)
|
|
3227
|
+
|
|
3228
|
+
return {
|
|
3229
|
+
"idea_id": side_effects_artifact.get("idea_id"),
|
|
3230
|
+
"passed": not final_errors,
|
|
3231
|
+
"iterations_used": iterations_used,
|
|
3232
|
+
"max_iterations": max_iterations,
|
|
3233
|
+
"stop_reason": str((validations[-1] or {}).get("stop_reason") or ("passed" if not final_errors else "max_iterations_exhausted")) if validations else "not_started",
|
|
3234
|
+
"checks": {
|
|
3235
|
+
"all_implicated_users_covered": not any("all implicated users covered" in error for error in final_errors),
|
|
3236
|
+
"all_side_effects_covered": not any("missing workflows for side effects" in error for error in final_errors),
|
|
3237
|
+
"story_alignment": not any("story alignment" in error for error in final_errors),
|
|
3238
|
+
"code_alignment": not any("code alignment" in error for error in final_errors),
|
|
3239
|
+
"interaction_points_complete": not any("interaction points missing" in error for error in final_errors),
|
|
3240
|
+
"acceptance_criteria_covered": not any("acceptance criteria uncovered" in error for error in final_errors),
|
|
3241
|
+
"acceptance_criteria_have_seams": not any("acceptance criterion missing seam" in error for error in final_errors),
|
|
3242
|
+
"idea_acceptance_criteria_covered": not any("idea acceptance criterion uncovered" in error for error in final_errors),
|
|
3243
|
+
"idea_acceptance_criteria_proven": not any("idea acceptance criterion under-proven" in error or "idea acceptance criterion drift" in error for error in final_errors),
|
|
3244
|
+
"idea_acceptance_builder_model_backed": not any("idea acceptance builder lineage missing model-backed proof" in error for error in final_errors),
|
|
3245
|
+
"no_invalid_actors_admitted": not any("invalid actors admitted" in error for error in final_errors),
|
|
3246
|
+
"agentic_review_clear": not blocking_agentic_findings,
|
|
3247
|
+
},
|
|
3248
|
+
"agentic_review": {
|
|
3249
|
+
"summary": agentic_review.get("summary"),
|
|
3250
|
+
"findings": agentic_findings,
|
|
3251
|
+
"blocking_findings": blocking_agentic_findings,
|
|
3252
|
+
"enriched_workflow_count": len(agentic_review.get("enriched_workflows") or pending),
|
|
3253
|
+
},
|
|
3254
|
+
"idea_acceptance_coverage": pending_idea_acceptance_coverage,
|
|
3255
|
+
"acceptance_criteria": {
|
|
3256
|
+
"idea_count": len(idea_acceptance_criteria),
|
|
3257
|
+
"story_count": sum(len(item.get("acceptance_criteria") or []) for item in story_acceptance.values()),
|
|
3258
|
+
"story_ids_with_acceptance": sorted(story_acceptance.keys()),
|
|
3259
|
+
"failing_idea_criterion_indices": sorted({int(item.get("criterion_index") or 0) for item in final_idea_acceptance_failures if int(item.get("criterion_index") or 0) > 0}),
|
|
3260
|
+
},
|
|
3261
|
+
"validation_history": validations,
|
|
3262
|
+
"errors": final_errors,
|
|
3263
|
+
"next_stages": [stage.node_id for stage in default_integration_stages()[3:]],
|
|
3264
|
+
"_final_workflows": pending,
|
|
3265
|
+
"_full_agentic_review": agentic_review,
|
|
3266
|
+
"_repair_clusters": final_repair_clusters_payload,
|
|
3267
|
+
}, iterations_used
|
|
3268
|
+
|
|
3269
|
+
def run_integration_dag(
|
|
3270
|
+
*,
|
|
3271
|
+
repo_root: Path,
|
|
3272
|
+
idea_id: str,
|
|
3273
|
+
implemented_idea: dict[str, Any],
|
|
3274
|
+
implemented_stories: list[dict[str, Any]],
|
|
3275
|
+
code_evidence: list[dict[str, Any]] | None = None,
|
|
3276
|
+
source_docs: list[dict[str, Any]] | None = None,
|
|
3277
|
+
project_id: str | None = None,
|
|
3278
|
+
dag_run_id: str | None = None,
|
|
3279
|
+
) -> IntegrationDagResult:
|
|
3280
|
+
code_evidence = list(code_evidence or [])
|
|
3281
|
+
source_docs = list(source_docs or [])
|
|
3282
|
+
resume_freshness = _compute_integration_freshness(
|
|
3283
|
+
repo_root=repo_root,
|
|
3284
|
+
idea_id=idea_id,
|
|
3285
|
+
payload_body={
|
|
3286
|
+
"idea_id": idea_id,
|
|
3287
|
+
"implemented_idea": implemented_idea,
|
|
3288
|
+
"implemented_stories": implemented_stories,
|
|
3289
|
+
"code_evidence": code_evidence,
|
|
3290
|
+
"source_docs": source_docs,
|
|
3291
|
+
},
|
|
3292
|
+
)
|
|
3293
|
+
runs_parent = _integration_runs_dir(repo_root, idea_id)
|
|
3294
|
+
runs_parent.mkdir(parents=True, exist_ok=True)
|
|
3295
|
+
pipeline_dir = runs_parent / f"run_{uuid.uuid4().hex[:12]}"
|
|
3296
|
+
pipeline_dir.mkdir(parents=True, exist_ok=True)
|
|
3297
|
+
|
|
3298
|
+
# Detect most useful prior run for checkpoint/resume
|
|
3299
|
+
prior_run_dir = _select_prior_run_dir(
|
|
3300
|
+
runs_parent,
|
|
3301
|
+
pipeline_dir,
|
|
3302
|
+
fallback_runs_parent=_legacy_integration_runs_dir(repo_root, idea_id),
|
|
3303
|
+
expected_fingerprint=str(resume_freshness.get("fingerprint") or "") or None,
|
|
3304
|
+
)
|
|
3305
|
+
|
|
3306
|
+
wf = IntegrationWorkflow()
|
|
3307
|
+
ctx = wf.run({
|
|
3308
|
+
"repo_root": str(repo_root),
|
|
3309
|
+
"idea_id": idea_id,
|
|
3310
|
+
"implemented_idea": implemented_idea,
|
|
3311
|
+
"implemented_stories": implemented_stories,
|
|
3312
|
+
"code_evidence": code_evidence,
|
|
3313
|
+
"source_docs": source_docs,
|
|
3314
|
+
"pipeline_dir": str(pipeline_dir),
|
|
3315
|
+
"prior_run_dir": str(prior_run_dir) if prior_run_dir else None,
|
|
3316
|
+
"resume_freshness": resume_freshness,
|
|
3317
|
+
"project_id": project_id,
|
|
3318
|
+
"dag_run_id": dag_run_id,
|
|
3319
|
+
})
|
|
3320
|
+
|
|
3321
|
+
artifacts = dict(ctx.metadata.get("artifacts") or {})
|
|
3322
|
+
validation_report = dict(ctx.metadata.get("validation_report") or {})
|
|
3323
|
+
iterations_used = int(ctx.metadata.get("iterations_used") or 0)
|
|
3324
|
+
if ctx.metadata.get("structural_only"):
|
|
3325
|
+
return IntegrationDagResult(
|
|
3326
|
+
exit_code=0,
|
|
3327
|
+
message="integration dag complete; structural-only idea (no integration seams)\n",
|
|
3328
|
+
pipeline_dir=pipeline_dir,
|
|
3329
|
+
artifacts=artifacts,
|
|
3330
|
+
iterations_used=iterations_used,
|
|
3331
|
+
)
|
|
3332
|
+
if not ctx.should_stop:
|
|
3333
|
+
return IntegrationDagResult(
|
|
3334
|
+
exit_code=0,
|
|
3335
|
+
message="integration dag complete; downstream red/redreview/green/greenenrich/commit artifacts written\n",
|
|
3336
|
+
pipeline_dir=pipeline_dir,
|
|
3337
|
+
artifacts=artifacts,
|
|
3338
|
+
iterations_used=iterations_used,
|
|
3339
|
+
)
|
|
3340
|
+
return IntegrationDagResult(
|
|
3341
|
+
exit_code=1,
|
|
3342
|
+
message="integration dag failed\n",
|
|
3343
|
+
pipeline_dir=pipeline_dir,
|
|
3344
|
+
artifacts=artifacts,
|
|
3345
|
+
iterations_used=iterations_used,
|
|
3346
|
+
)
|
|
3347
|
+
|
|
3348
|
+
|
|
3349
|
+
def backfill_integration_current(*, repo_root: Path, idea_id: str) -> dict[str, Any]:
|
|
3350
|
+
current_dir = _integration_current_dir(repo_root, idea_id)
|
|
3351
|
+
current_dir.mkdir(parents=True, exist_ok=True)
|
|
3352
|
+
runs_dir = _integration_runs_dir(repo_root, idea_id)
|
|
3353
|
+
legacy_runs_dir = _legacy_integration_runs_dir(repo_root, idea_id)
|
|
3354
|
+
payload = json.loads(prepare_integration_payload(repo_root=repo_root, idea_id=idea_id).read_text(encoding="utf-8"))
|
|
3355
|
+
resume_freshness = _compute_integration_freshness(
|
|
3356
|
+
repo_root=repo_root,
|
|
3357
|
+
idea_id=idea_id,
|
|
3358
|
+
payload_body=_payload_body_without_freshness(payload),
|
|
3359
|
+
)
|
|
3360
|
+
selected = _select_prior_run_dir(
|
|
3361
|
+
runs_dir,
|
|
3362
|
+
current_dir / "_ignore",
|
|
3363
|
+
fallback_runs_parent=legacy_runs_dir,
|
|
3364
|
+
expected_fingerprint=str(resume_freshness.get("fingerprint") or "") or None,
|
|
3365
|
+
)
|
|
3366
|
+
copied: list[str] = []
|
|
3367
|
+
if selected is None:
|
|
3368
|
+
return {
|
|
3369
|
+
"idea_id": idea_id,
|
|
3370
|
+
"selected_run": None,
|
|
3371
|
+
"current_dir": str(current_dir),
|
|
3372
|
+
"copied": copied,
|
|
3373
|
+
}
|
|
3374
|
+
for path in sorted(selected.iterdir()):
|
|
3375
|
+
if not path.is_file() or path.name.endswith("_agent_run.json"):
|
|
3376
|
+
continue
|
|
3377
|
+
target = current_dir / path.name
|
|
3378
|
+
try:
|
|
3379
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
3380
|
+
except Exception:
|
|
3381
|
+
continue
|
|
3382
|
+
_write_json(target, data)
|
|
3383
|
+
copied.append(path.name)
|
|
3384
|
+
_write_resume_metadata(current_dir, resume_freshness)
|
|
3385
|
+
return {
|
|
3386
|
+
"idea_id": idea_id,
|
|
3387
|
+
"selected_run": str(selected),
|
|
3388
|
+
"current_dir": str(current_dir),
|
|
3389
|
+
"copied": copied,
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
|
|
3393
|
+
def _describe_postgrest_exception(exc: Exception) -> str:
|
|
3394
|
+
if isinstance(exc, HTTPError):
|
|
3395
|
+
try:
|
|
3396
|
+
body = exc.read().decode("utf-8", errors="replace").strip()
|
|
3397
|
+
except Exception:
|
|
3398
|
+
body = ""
|
|
3399
|
+
if body:
|
|
3400
|
+
return f"HTTP {exc.code}: {body}"
|
|
3401
|
+
return f"HTTP {exc.code}: {exc.reason}"
|
|
3402
|
+
return str(exc) or exc.__class__.__name__
|
|
3403
|
+
|
|
3404
|
+
|
|
3405
|
+
def _sync_integration_to_supabase(
|
|
3406
|
+
*,
|
|
3407
|
+
idea_id: str,
|
|
3408
|
+
project_id: str,
|
|
3409
|
+
run_id: str,
|
|
3410
|
+
repo_root: Path | None = None,
|
|
3411
|
+
result: IntegrationDagResult | None = None,
|
|
3412
|
+
status: str = "running",
|
|
3413
|
+
) -> None:
|
|
3414
|
+
config = _resolve_supabase_rest_config()
|
|
3415
|
+
if config is None:
|
|
3416
|
+
return
|
|
3417
|
+
if not project_id or project_id.startswith("unregistered:"):
|
|
3418
|
+
return
|
|
3419
|
+
url, key = config
|
|
3420
|
+
resolved_project_id = str(project_id or "").strip()
|
|
3421
|
+
if not _is_uuid_like(resolved_project_id):
|
|
3422
|
+
if repo_root is None:
|
|
3423
|
+
project_entry = resolve_project_entry(project_id)
|
|
3424
|
+
repo_root_value = str((project_entry or {}).get("repo_root") or "").strip()
|
|
3425
|
+
if repo_root_value:
|
|
3426
|
+
repo_root = Path(repo_root_value).expanduser()
|
|
3427
|
+
if repo_root is None:
|
|
3428
|
+
logger.warning(
|
|
3429
|
+
"integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because local project_id=%s is not canonical and repo_root is unavailable",
|
|
3430
|
+
idea_id,
|
|
3431
|
+
run_id,
|
|
3432
|
+
project_id,
|
|
3433
|
+
)
|
|
3434
|
+
return
|
|
3435
|
+
project_entry = find_project_for_repo_root(repo_root)
|
|
3436
|
+
remote_url = str((project_entry or {}).get("remote_url") or "").strip() or None
|
|
3437
|
+
project_name = str((project_entry or {}).get("name") or "").strip() or None
|
|
3438
|
+
owner, repo = _infer_owner_repo(remote_url, repo_root, project_name)
|
|
3439
|
+
try:
|
|
3440
|
+
resolved_project_id = _lookup_supabase_project_uuid(
|
|
3441
|
+
url=url,
|
|
3442
|
+
key=key,
|
|
3443
|
+
repo_root=repo_root,
|
|
3444
|
+
remote_url=remote_url,
|
|
3445
|
+
owner=owner,
|
|
3446
|
+
repo=repo,
|
|
3447
|
+
) or ""
|
|
3448
|
+
except Exception as exc:
|
|
3449
|
+
logger.warning(
|
|
3450
|
+
"integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because canonical project UUID lookup failed for local project_id=%s: %s",
|
|
3451
|
+
idea_id,
|
|
3452
|
+
run_id,
|
|
3453
|
+
project_id,
|
|
3454
|
+
exc,
|
|
3455
|
+
)
|
|
3456
|
+
return
|
|
3457
|
+
if not _is_uuid_like(resolved_project_id):
|
|
3458
|
+
logger.warning(
|
|
3459
|
+
"integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because canonical project UUID could not be resolved from local project_id=%s",
|
|
3460
|
+
idea_id,
|
|
3461
|
+
run_id,
|
|
3462
|
+
project_id,
|
|
3463
|
+
)
|
|
3464
|
+
return
|
|
3465
|
+
from datetime import UTC, datetime
|
|
3466
|
+
|
|
3467
|
+
now = datetime.now(UTC).isoformat()
|
|
3468
|
+
|
|
3469
|
+
def _load_artifact(key_name: str) -> dict[str, Any] | None:
|
|
3470
|
+
if result is None:
|
|
3471
|
+
return None
|
|
3472
|
+
path_str = result.artifacts.get(key_name)
|
|
3473
|
+
if not path_str:
|
|
3474
|
+
return None
|
|
3475
|
+
p = Path(path_str)
|
|
3476
|
+
if not p.exists():
|
|
3477
|
+
return None
|
|
3478
|
+
try:
|
|
3479
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
3480
|
+
except Exception:
|
|
3481
|
+
return None
|
|
3482
|
+
|
|
3483
|
+
side_effects = _load_artifact("side_effects")
|
|
3484
|
+
implicated_users = _load_artifact("implicated_users")
|
|
3485
|
+
workflow_inventory = _load_artifact("workflow_inventory")
|
|
3486
|
+
validation_report = _load_artifact("validation_gate")
|
|
3487
|
+
red_package = _load_artifact("red_package")
|
|
3488
|
+
red_review = _load_artifact("red_review")
|
|
3489
|
+
green_package = _load_artifact("green_package")
|
|
3490
|
+
green_enrich = _load_artifact("green_enrich")
|
|
3491
|
+
commit_package = _load_artifact("commit_package")
|
|
3492
|
+
|
|
3493
|
+
side_effect_count = len((side_effects or {}).get("side_effects") or []) if side_effects else 0
|
|
3494
|
+
workflow_count = len((workflow_inventory or {}).get("workflow_ids") or []) if workflow_inventory else 0
|
|
3495
|
+
|
|
3496
|
+
effective_status = status
|
|
3497
|
+
if validation_report and validation_report.get("structural_only"):
|
|
3498
|
+
effective_status = "structural_only"
|
|
3499
|
+
|
|
3500
|
+
row = {
|
|
3501
|
+
"idea_id": idea_id,
|
|
3502
|
+
"project_id": resolved_project_id,
|
|
3503
|
+
"run_id": run_id,
|
|
3504
|
+
"pipeline_dir": str(result.pipeline_dir) if result is not None else None,
|
|
3505
|
+
"status": effective_status,
|
|
3506
|
+
"exit_code": result.exit_code if result is not None else None,
|
|
3507
|
+
"iterations_used": result.iterations_used if result is not None else None,
|
|
3508
|
+
"workflow_count": workflow_count,
|
|
3509
|
+
"side_effect_count": side_effect_count,
|
|
3510
|
+
"side_effects": side_effects,
|
|
3511
|
+
"implicated_users": implicated_users,
|
|
3512
|
+
"workflow_inventory": workflow_inventory,
|
|
3513
|
+
"validation_report": validation_report,
|
|
3514
|
+
"red_package": red_package,
|
|
3515
|
+
"red_review": red_review,
|
|
3516
|
+
"green_package": green_package,
|
|
3517
|
+
"green_enrich": green_enrich,
|
|
3518
|
+
"commit_package": commit_package,
|
|
3519
|
+
"failure_message": result.message if (result is not None and result.exit_code != 0) else None,
|
|
3520
|
+
"updated_at": now,
|
|
3521
|
+
}
|
|
3522
|
+
try:
|
|
3523
|
+
_postgrest_request(
|
|
3524
|
+
method="POST",
|
|
3525
|
+
url=f"{url}/rest/v1/devflow_idea_integrations?on_conflict=idea_id",
|
|
3526
|
+
key=key,
|
|
3527
|
+
body=[row],
|
|
3528
|
+
prefer="resolution=merge-duplicates",
|
|
3529
|
+
)
|
|
3530
|
+
except Exception as exc:
|
|
3531
|
+
logger.warning(
|
|
3532
|
+
"integration-supabase-sync: failed for idea_id=%s run_id=%s status=%s project_id=%s row_keys=%s error=%s",
|
|
3533
|
+
idea_id,
|
|
3534
|
+
run_id,
|
|
3535
|
+
effective_status,
|
|
3536
|
+
resolved_project_id,
|
|
3537
|
+
sorted(row.keys()),
|
|
3538
|
+
_describe_postgrest_exception(exc),
|
|
3539
|
+
)
|