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,1606 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, field_validator
|
|
14
|
+
|
|
15
|
+
from .agentic_prompts import load_agentic_prompt_lines
|
|
16
|
+
from .agentic_runtime import run_agent_step
|
|
17
|
+
from .source_doc_assumptions import reconcile_assumptions_registry, register_assumption
|
|
18
|
+
from .source_docs_schema import source_doc_template_payloads
|
|
19
|
+
from .source_docs_updater import ensure_source_doc_scaffold
|
|
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.nodes.concurrent import ConcurrentNode
|
|
23
|
+
from .vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
|
|
24
|
+
from .vendor.datalumina_genai.core.task import TaskContext
|
|
25
|
+
from .vendor.datalumina_genai.core.workflow import Workflow
|
|
26
|
+
|
|
27
|
+
DAG_ID = "source_doc_mutation_dag"
|
|
28
|
+
CANONICAL_SOURCE_DOCS = tuple(source_doc_template_payloads().keys())
|
|
29
|
+
PROJECT_DOC_TARGETS = (
|
|
30
|
+
"project_charter.md",
|
|
31
|
+
"prd.md",
|
|
32
|
+
"architecture.md",
|
|
33
|
+
"ux_design.md",
|
|
34
|
+
"delivery_plan.md",
|
|
35
|
+
)
|
|
36
|
+
PROJECT_DOC_REFERENCE_DOCS = {
|
|
37
|
+
"richness_contract": "docs/project-doc-richness-contract.md",
|
|
38
|
+
"agent_contracts": "docs/project-doc-agent-contracts.md",
|
|
39
|
+
"reference_map": "docs/source-to-project-doc-reference-map.md",
|
|
40
|
+
"cross_reference_rules": "docs/project-doc-cross-reference-rules.md",
|
|
41
|
+
}
|
|
42
|
+
SUPPORT_INDEX_DOCS = {
|
|
43
|
+
"internal_source": "docs/support/source_doc_mutation/internal_source.json",
|
|
44
|
+
"internal_project": "docs/support/source_doc_mutation/internal_project.json",
|
|
45
|
+
"cross_sourceXproject": "docs/support/source_doc_mutation/cross_sourceXproject.json",
|
|
46
|
+
}
|
|
47
|
+
PROJECT_DOC_PRIMARY_SOURCE_DOCS = {
|
|
48
|
+
"project_charter.md": ["product_brief.json", "assumptions_registry.json"],
|
|
49
|
+
"prd.md": ["product_brief.json", "user_workflows.json", "assumptions_registry.json"],
|
|
50
|
+
"architecture.md": ["domain_entities.json", "product_brief.json", "assumptions_registry.json"],
|
|
51
|
+
"ux_design.md": ["user_workflows.json", "product_brief.json", "assumptions_registry.json"],
|
|
52
|
+
"delivery_plan.md": ["product_brief.json", "user_workflows.json", "domain_entities.json", "assumptions_registry.json"],
|
|
53
|
+
}
|
|
54
|
+
PROJECT_DOC_SECTION_IMPACT_MAP = {
|
|
55
|
+
"product_brief.json": ["project_charter.md", "prd.md", "architecture.md", "ux_design.md", "delivery_plan.md"],
|
|
56
|
+
"user_workflows.json": ["prd.md", "architecture.md", "ux_design.md", "delivery_plan.md"],
|
|
57
|
+
"domain_entities.json": ["prd.md", "architecture.md", "ux_design.md", "delivery_plan.md"],
|
|
58
|
+
"assumptions_registry.json": list(PROJECT_DOC_TARGETS),
|
|
59
|
+
}
|
|
60
|
+
REPO_BOOTSTRAP_ALLOWED_SUFFIXES = {
|
|
61
|
+
".md",
|
|
62
|
+
".mdx",
|
|
63
|
+
".txt",
|
|
64
|
+
".rst",
|
|
65
|
+
".py",
|
|
66
|
+
".ts",
|
|
67
|
+
".tsx",
|
|
68
|
+
".js",
|
|
69
|
+
".jsx",
|
|
70
|
+
".json",
|
|
71
|
+
".yaml",
|
|
72
|
+
".yml",
|
|
73
|
+
".toml",
|
|
74
|
+
}
|
|
75
|
+
REPO_BOOTSTRAP_IGNORE_DIRS = {
|
|
76
|
+
".git",
|
|
77
|
+
".hg",
|
|
78
|
+
".svn",
|
|
79
|
+
".venv",
|
|
80
|
+
"venv",
|
|
81
|
+
"node_modules",
|
|
82
|
+
"dist",
|
|
83
|
+
"build",
|
|
84
|
+
".next",
|
|
85
|
+
".turbo",
|
|
86
|
+
".pytest_cache",
|
|
87
|
+
"__pycache__",
|
|
88
|
+
".mypy_cache",
|
|
89
|
+
".ruff_cache",
|
|
90
|
+
".devflow",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class SourceDocMutationDagResult:
|
|
96
|
+
run_id: str
|
|
97
|
+
pipeline_dir: Path
|
|
98
|
+
mutation_ref: dict[str, Any]
|
|
99
|
+
result: dict[str, Any]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SourceDocMutationDagEvent(BaseModel):
|
|
103
|
+
project_id: str
|
|
104
|
+
idea_id: str
|
|
105
|
+
repo_root: str
|
|
106
|
+
raw_text: str = ""
|
|
107
|
+
grounded_raw_text: str = ""
|
|
108
|
+
mode: str | None = None
|
|
109
|
+
agent_runtime_mode: str | None = None
|
|
110
|
+
history_grounding_ref: str | None = None
|
|
111
|
+
existing_source_doc_refs: list[str] = []
|
|
112
|
+
explicit_source_doc_payload: dict[str, Any] | None = None
|
|
113
|
+
message_id: str | None = None
|
|
114
|
+
session_id: str | None = None
|
|
115
|
+
invoked_from: str = "idea_context_resolution"
|
|
116
|
+
pipeline_key: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _stable_hash(payload: Any) -> str:
|
|
120
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _stable_id(prefix: str, payload: Any, *, size: int = 12) -> str:
|
|
124
|
+
return f"{prefix}{_stable_hash(payload)[:size]}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _clone(value: Any) -> Any:
|
|
128
|
+
return json.loads(json.dumps(value))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
132
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_json_dict(path: Path, *, default: dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
if not path.exists():
|
|
138
|
+
return _clone(default)
|
|
139
|
+
try:
|
|
140
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
141
|
+
except Exception:
|
|
142
|
+
return _clone(default)
|
|
143
|
+
return payload if isinstance(payload, dict) else _clone(default)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _source_docs_dir(repo_root: Path) -> Path:
|
|
147
|
+
return repo_root / "ai_docs" / "context" / "source_docs"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _v2_project_docs_dir(repo_root: Path) -> Path:
|
|
151
|
+
return repo_root / "ai_docs" / "context" / "v2" / "project_docs"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _pipeline_root(repo_root: Path, *, idea_id: str, pipeline_key: str) -> Path:
|
|
155
|
+
safe_idea_id = idea_id or "bootstrap"
|
|
156
|
+
return repo_root / ".devflow" / "ideas" / safe_idea_id / "pipelines" / DAG_ID / pipeline_key
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _dedupe_str_list(values: list[Any]) -> list[str]:
|
|
160
|
+
out: list[str] = []
|
|
161
|
+
for item in values:
|
|
162
|
+
if not isinstance(item, str):
|
|
163
|
+
continue
|
|
164
|
+
text = item.strip()
|
|
165
|
+
if text and text not in out:
|
|
166
|
+
out.append(text)
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _guess_doc_type_from_message(raw_text: str) -> str:
|
|
171
|
+
lowered = raw_text.lower()
|
|
172
|
+
for token, label in [
|
|
173
|
+
("portal", "portal"),
|
|
174
|
+
("dashboard", "dashboard"),
|
|
175
|
+
("app", "app"),
|
|
176
|
+
("api", "API"),
|
|
177
|
+
("workflow", "workflow automation"),
|
|
178
|
+
("system", "system"),
|
|
179
|
+
("platform", "platform"),
|
|
180
|
+
]:
|
|
181
|
+
if token in lowered:
|
|
182
|
+
return label
|
|
183
|
+
return "software product"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _infer_users(raw_text: str) -> list[str]:
|
|
187
|
+
lowered = raw_text.lower()
|
|
188
|
+
tokens = {token.strip(".,:;!?()[]{}\"'`") for token in lowered.split()}
|
|
189
|
+
users: list[str] = []
|
|
190
|
+
if "leadership" in tokens or "executive" in tokens or "executives" in tokens:
|
|
191
|
+
users.append("leadership")
|
|
192
|
+
if "board" in tokens:
|
|
193
|
+
users.append("board members")
|
|
194
|
+
if "office" in lowered and "field" in lowered:
|
|
195
|
+
users.append("team members")
|
|
196
|
+
if "team" in lowered:
|
|
197
|
+
if "service team" in lowered:
|
|
198
|
+
users.append("service team")
|
|
199
|
+
elif "support team" in lowered:
|
|
200
|
+
users.append("support team")
|
|
201
|
+
else:
|
|
202
|
+
users.append("team members")
|
|
203
|
+
if "operator" in lowered:
|
|
204
|
+
users.append("operators")
|
|
205
|
+
return list(dict.fromkeys(users))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _infer_scope_bullets(raw_text: str) -> list[str]:
|
|
209
|
+
lowered = raw_text.lower()
|
|
210
|
+
bullets: list[str] = []
|
|
211
|
+
for token, label in [
|
|
212
|
+
("assignment", "task assignments"),
|
|
213
|
+
("due date", "due dates"),
|
|
214
|
+
("dashboard", "dashboard view"),
|
|
215
|
+
("route", "routing"),
|
|
216
|
+
("queue", "review queue"),
|
|
217
|
+
("report", "report generation"),
|
|
218
|
+
("trend", "trend analysis"),
|
|
219
|
+
]:
|
|
220
|
+
if token in lowered:
|
|
221
|
+
bullets.append(label)
|
|
222
|
+
return list(dict.fromkeys(bullets))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _infer_problem(raw_text: str) -> str | None:
|
|
226
|
+
lowered = raw_text.lower()
|
|
227
|
+
if "managing tasks" in lowered or "manage tasks" in lowered:
|
|
228
|
+
return "The team needs a simpler way to manage and track work."
|
|
229
|
+
if "assignments" in lowered or "due dates" in lowered:
|
|
230
|
+
return "The team lacks a clear system for tracking assignments and due dates."
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _infer_goal(raw_text: str) -> str | None:
|
|
235
|
+
lowered = raw_text.lower()
|
|
236
|
+
if "dashboard" in lowered and ("assignment" in lowered or "due date" in lowered):
|
|
237
|
+
return "Give the team one place to track assignments, due dates, and task status."
|
|
238
|
+
if "managing tasks" in lowered or "manage tasks" in lowered:
|
|
239
|
+
return "Create a simple app for organizing team tasks."
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def extract_sufficient_idea(raw_text: str) -> dict[str, Any]:
|
|
244
|
+
users = _infer_users(raw_text)
|
|
245
|
+
problem = _infer_problem(raw_text) or ""
|
|
246
|
+
goal = _infer_goal(raw_text) or ""
|
|
247
|
+
scope = _infer_scope_bullets(raw_text)
|
|
248
|
+
summary = raw_text.strip().splitlines()[0][:240] if raw_text.strip() else ""
|
|
249
|
+
return {
|
|
250
|
+
"summary": summary,
|
|
251
|
+
"problem": problem,
|
|
252
|
+
"goal": goal,
|
|
253
|
+
"target_users": users,
|
|
254
|
+
"scope": scope,
|
|
255
|
+
"constraints": [],
|
|
256
|
+
"acceptance_criteria": [],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _derive_product_summary(raw_text: str, sufficient_idea: dict[str, Any]) -> str:
|
|
261
|
+
summary = str(sufficient_idea.get("summary") or "").strip()
|
|
262
|
+
if summary:
|
|
263
|
+
return summary
|
|
264
|
+
users = _dedupe_str_list(list(sufficient_idea.get("target_users") or [])) or _infer_users(raw_text)
|
|
265
|
+
goal = str(sufficient_idea.get("goal") or _infer_goal(raw_text) or "").strip()
|
|
266
|
+
doc_type = _guess_doc_type_from_message(raw_text)
|
|
267
|
+
if users and goal:
|
|
268
|
+
lowered_goal = goal[0].lower() + goal[1:] if len(goal) > 1 else goal.lower()
|
|
269
|
+
return f"{doc_type.capitalize()} for {users[0]} so they can {lowered_goal}."
|
|
270
|
+
if users:
|
|
271
|
+
return f"{doc_type.capitalize()} for {users[0]}."
|
|
272
|
+
return raw_text.strip().splitlines()[0][:240] if raw_text.strip() else ""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _load_source_doc_state(repo_root: Path) -> dict[str, dict[str, Any]]:
|
|
276
|
+
source_docs_dir = ensure_source_doc_scaffold(repo_root).source_docs_dir
|
|
277
|
+
return {
|
|
278
|
+
name: _load_json_dict(source_docs_dir / name, default=payload)
|
|
279
|
+
for name, payload in source_doc_template_payloads().items()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _record_assumption(
|
|
284
|
+
*,
|
|
285
|
+
assumptions: list[dict[str, Any]],
|
|
286
|
+
doc_path: str,
|
|
287
|
+
section_key: str,
|
|
288
|
+
topic_key: str,
|
|
289
|
+
current_value: Any,
|
|
290
|
+
reasoning: str,
|
|
291
|
+
source: str,
|
|
292
|
+
message_id: str,
|
|
293
|
+
) -> None:
|
|
294
|
+
registry = register_assumption(
|
|
295
|
+
{"assumptions": assumptions},
|
|
296
|
+
doc_path=doc_path,
|
|
297
|
+
section_key=section_key,
|
|
298
|
+
topic_key=topic_key,
|
|
299
|
+
current_value=current_value,
|
|
300
|
+
reasoning=reasoning,
|
|
301
|
+
source=source,
|
|
302
|
+
message_id=message_id,
|
|
303
|
+
)
|
|
304
|
+
assumptions[:] = [item for item in registry.get("assumptions") or [] if isinstance(item, dict)]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _render_markdown(doc_name: str, payload: Any, *, shell_only: bool = False) -> str:
|
|
308
|
+
title = doc_name.replace("_", " ").replace(".json", "").title()
|
|
309
|
+
pretty_json = json.dumps(payload, indent=2, sort_keys=True)
|
|
310
|
+
summary_lines = [f"# {title}", "", f"Derived from `ai_docs/context/source_docs/{doc_name}`.", ""]
|
|
311
|
+
if shell_only:
|
|
312
|
+
summary_lines.extend([
|
|
313
|
+
"## Scaffold Status",
|
|
314
|
+
"",
|
|
315
|
+
"Minimal scaffold-only placeholder. Repo evidence was too sparse to ground this doc yet.",
|
|
316
|
+
"",
|
|
317
|
+
])
|
|
318
|
+
if isinstance(payload, dict):
|
|
319
|
+
for key, value in payload.items():
|
|
320
|
+
summary_lines.append(f"## {str(key).replace('_', ' ').title()}")
|
|
321
|
+
if isinstance(value, list):
|
|
322
|
+
if value:
|
|
323
|
+
for item in value:
|
|
324
|
+
summary_lines.append(
|
|
325
|
+
f"- {json.dumps(item, sort_keys=True) if isinstance(item, (dict, list)) else item}"
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
summary_lines.append("- _empty_")
|
|
329
|
+
elif isinstance(value, dict):
|
|
330
|
+
if value:
|
|
331
|
+
for sub_key, sub_value in value.items():
|
|
332
|
+
rendered = json.dumps(sub_value, sort_keys=True) if isinstance(sub_value, (dict, list)) else sub_value
|
|
333
|
+
summary_lines.append(f"- **{sub_key}**: {rendered}")
|
|
334
|
+
else:
|
|
335
|
+
summary_lines.append("- _empty_")
|
|
336
|
+
else:
|
|
337
|
+
summary_lines.append(str(value) if value not in (None, "") else "_empty_")
|
|
338
|
+
summary_lines.append("")
|
|
339
|
+
summary_lines.extend(["## Canonical JSON", "", "```json", pretty_json, "```", ""])
|
|
340
|
+
return "\n".join(summary_lines)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _compute_assumptions_delta(before: list[dict[str, Any]], after: list[dict[str, Any]]) -> dict[str, Any]:
|
|
344
|
+
before_by_id = {str(item.get("assumption_id") or item.get("assumption_key") or ""): item for item in before if isinstance(item, dict)}
|
|
345
|
+
after_by_id = {str(item.get("assumption_id") or item.get("assumption_key") or ""): item for item in after if isinstance(item, dict)}
|
|
346
|
+
added = [item for key, item in after_by_id.items() if key and key not in before_by_id]
|
|
347
|
+
removed = [item for key, item in before_by_id.items() if key and key not in after_by_id]
|
|
348
|
+
status_changes: list[dict[str, Any]] = []
|
|
349
|
+
for key, item in after_by_id.items():
|
|
350
|
+
if key not in before_by_id:
|
|
351
|
+
continue
|
|
352
|
+
prior = before_by_id[key]
|
|
353
|
+
if str(prior.get("status") or "") != str(item.get("status") or ""):
|
|
354
|
+
status_changes.append(
|
|
355
|
+
{
|
|
356
|
+
"assumption_id": key,
|
|
357
|
+
"assumption_key": item.get("assumption_key"),
|
|
358
|
+
"from_status": prior.get("status"),
|
|
359
|
+
"to_status": item.get("status"),
|
|
360
|
+
"doc_path": item.get("doc_path"),
|
|
361
|
+
"section_key": item.get("section_key"),
|
|
362
|
+
"topic_key": item.get("topic_key"),
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
return {
|
|
366
|
+
"before_count": len(before_by_id),
|
|
367
|
+
"after_count": len(after_by_id),
|
|
368
|
+
"added": added,
|
|
369
|
+
"removed": removed,
|
|
370
|
+
"status_changes": status_changes,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _sync_v2_project_docs(
|
|
375
|
+
*,
|
|
376
|
+
repo_root: Path,
|
|
377
|
+
changed_docs: list[str],
|
|
378
|
+
source_docs: dict[str, dict[str, Any]],
|
|
379
|
+
shell_only: bool = False,
|
|
380
|
+
sync_reason: str = "deterministic_markdown_render",
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
v2_root = _v2_project_docs_dir(repo_root)
|
|
383
|
+
source_doc_root = v2_root / "source_docs"
|
|
384
|
+
source_doc_root.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
updated_outputs: list[str] = []
|
|
386
|
+
source_hashes: dict[str, str] = {}
|
|
387
|
+
|
|
388
|
+
for doc_name, payload in sorted(source_docs.items()):
|
|
389
|
+
source_hashes[doc_name] = _stable_hash(payload)
|
|
390
|
+
md_path = source_doc_root / doc_name.replace(".json", ".md")
|
|
391
|
+
md_path.write_text(_render_markdown(doc_name, payload, shell_only=shell_only), encoding="utf-8")
|
|
392
|
+
updated_outputs.append(str(md_path.relative_to(repo_root)))
|
|
393
|
+
|
|
394
|
+
index_title = "# Canonical Source Docs Sync Index"
|
|
395
|
+
index_intro = "This directory contains derived project docs generated from canonical source docs under `ai_docs/context/source_docs/`."
|
|
396
|
+
if shell_only:
|
|
397
|
+
index_intro = (
|
|
398
|
+
"This directory contains scaffold-only derived project doc shells. Repo evidence was too sparse to infer a grounded project shape."
|
|
399
|
+
)
|
|
400
|
+
index_lines = [index_title, "", index_intro, "", "## Source Docs", ""]
|
|
401
|
+
for doc_name in sorted(source_docs):
|
|
402
|
+
md_name = doc_name.replace(".json", ".md")
|
|
403
|
+
marker = "updated" if doc_name in changed_docs else "current"
|
|
404
|
+
index_lines.append(f"- `source_docs/{md_name}` ← `{doc_name}` ({marker})")
|
|
405
|
+
index_lines.extend(["", "## Source Hashes", ""])
|
|
406
|
+
for doc_name, digest in sorted(source_hashes.items()):
|
|
407
|
+
index_lines.append(f"- `{doc_name}`: `{digest}`")
|
|
408
|
+
index_path = source_doc_root / "index.md"
|
|
409
|
+
index_path.write_text("\n".join(index_lines) + "\n", encoding="utf-8")
|
|
410
|
+
updated_outputs.append(str(index_path.relative_to(repo_root)))
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"status": "completed",
|
|
414
|
+
"derived_project_docs_root": str(v2_root),
|
|
415
|
+
"derived_source_doc_root": str(source_doc_root),
|
|
416
|
+
"updated_outputs": updated_outputs,
|
|
417
|
+
"changed_docs": changed_docs,
|
|
418
|
+
"source_hashes": source_hashes,
|
|
419
|
+
"sync_mode": sync_reason,
|
|
420
|
+
"shell_only": shell_only,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _list_repo_bootstrap_candidates(repo_root: Path) -> list[Path]:
|
|
425
|
+
candidates: list[Path] = []
|
|
426
|
+
for path in sorted(repo_root.rglob("*")):
|
|
427
|
+
if not path.is_file():
|
|
428
|
+
continue
|
|
429
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
430
|
+
if any(part in REPO_BOOTSTRAP_IGNORE_DIRS for part in path.parts):
|
|
431
|
+
continue
|
|
432
|
+
if rel.startswith("ai_docs/context/source_docs/") or rel.startswith("ai_docs/context/v2/project_docs/") or rel.startswith("ai_docs/context/project_docs/"):
|
|
433
|
+
continue
|
|
434
|
+
if path.suffix.lower() not in REPO_BOOTSTRAP_ALLOWED_SUFFIXES:
|
|
435
|
+
continue
|
|
436
|
+
candidates.append(path)
|
|
437
|
+
return candidates
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _read_text_safely(path: Path, *, max_chars: int = 4000) -> str:
|
|
441
|
+
try:
|
|
442
|
+
text = path.read_text(encoding="utf-8")
|
|
443
|
+
except Exception:
|
|
444
|
+
return ""
|
|
445
|
+
return text[:max_chars]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _repo_bootstrap_inventory(repo_root: Path) -> dict[str, Any]:
|
|
449
|
+
candidates = _list_repo_bootstrap_candidates(repo_root)
|
|
450
|
+
evidence_files: list[dict[str, Any]] = []
|
|
451
|
+
matched_signal_files = 0
|
|
452
|
+
aggregate_tokens: set[str] = set()
|
|
453
|
+
signal_hits = {
|
|
454
|
+
"readme": 0,
|
|
455
|
+
"docs": 0,
|
|
456
|
+
"product_terms": 0,
|
|
457
|
+
"workflow_terms": 0,
|
|
458
|
+
"entity_terms": 0,
|
|
459
|
+
"scope_terms": 0,
|
|
460
|
+
}
|
|
461
|
+
product_terms = ("dashboard", "portal", "platform", "workflow", "app", "api", "service", "report")
|
|
462
|
+
workflow_terms = ("user", "users", "actor", "workflow", "journey", "task", "queue", "assign")
|
|
463
|
+
entity_terms = ("entity", "entities", "model", "models", "schema", "record", "task", "report")
|
|
464
|
+
scope_terms = ("feature", "features", "mvp", "scope", "requirements", "acceptance", "goal")
|
|
465
|
+
|
|
466
|
+
for path in candidates[:24]:
|
|
467
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
468
|
+
text = _read_text_safely(path)
|
|
469
|
+
lowered = text.lower()
|
|
470
|
+
hit_count = 0
|
|
471
|
+
if path.name.lower().startswith("readme"):
|
|
472
|
+
signal_hits["readme"] += 1
|
|
473
|
+
hit_count += 1
|
|
474
|
+
if "docs/" in rel or rel.startswith("docs/") or "ai_docs/" in rel or rel.startswith("ai_docs/"):
|
|
475
|
+
signal_hits["docs"] += 1
|
|
476
|
+
hit_count += 1
|
|
477
|
+
for key, tokens in (
|
|
478
|
+
("product_terms", product_terms),
|
|
479
|
+
("workflow_terms", workflow_terms),
|
|
480
|
+
("entity_terms", entity_terms),
|
|
481
|
+
("scope_terms", scope_terms),
|
|
482
|
+
):
|
|
483
|
+
token_hits = [token for token in tokens if token in lowered]
|
|
484
|
+
if token_hits:
|
|
485
|
+
signal_hits[key] += 1
|
|
486
|
+
hit_count += 1
|
|
487
|
+
aggregate_tokens.update(token_hits)
|
|
488
|
+
if hit_count:
|
|
489
|
+
matched_signal_files += 1
|
|
490
|
+
evidence_files.append({"path": rel, "matched_signal_count": hit_count, "size_bytes": path.stat().st_size})
|
|
491
|
+
|
|
492
|
+
has_genuine_signal = (
|
|
493
|
+
signal_hits["readme"] >= 1
|
|
494
|
+
and matched_signal_files >= 2
|
|
495
|
+
and len(aggregate_tokens) >= 3
|
|
496
|
+
and (signal_hits["product_terms"] + signal_hits["workflow_terms"] + signal_hits["entity_terms"] + signal_hits["scope_terms"]) >= 3
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
"candidate_file_count": len(candidates),
|
|
501
|
+
"inspected_file_count": min(len(candidates), 24),
|
|
502
|
+
"matched_signal_files": matched_signal_files,
|
|
503
|
+
"signal_hits": signal_hits,
|
|
504
|
+
"aggregate_tokens": sorted(aggregate_tokens),
|
|
505
|
+
"evidence_files": evidence_files,
|
|
506
|
+
"has_genuine_signal": has_genuine_signal,
|
|
507
|
+
"decision_rule": {
|
|
508
|
+
"requires_readme": True,
|
|
509
|
+
"minimum_signal_files": 2,
|
|
510
|
+
"minimum_distinct_tokens": 3,
|
|
511
|
+
"minimum_signal_buckets": 3,
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _pick_repo_grounding_text(repo_root: Path) -> tuple[str, list[str]]:
|
|
517
|
+
preferred_names = ("README.md", "README.mdx", "README.txt")
|
|
518
|
+
snippets: list[str] = []
|
|
519
|
+
evidence_paths: list[str] = []
|
|
520
|
+
for name in preferred_names:
|
|
521
|
+
path = repo_root / name
|
|
522
|
+
if path.exists() and path.is_file():
|
|
523
|
+
text = _read_text_safely(path, max_chars=2500).strip()
|
|
524
|
+
if text:
|
|
525
|
+
snippets.append(text)
|
|
526
|
+
evidence_paths.append(path.relative_to(repo_root).as_posix())
|
|
527
|
+
break
|
|
528
|
+
for path in _list_repo_bootstrap_candidates(repo_root):
|
|
529
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
530
|
+
if rel in evidence_paths:
|
|
531
|
+
continue
|
|
532
|
+
if not (rel.startswith("docs/") or rel.startswith("ai_docs/")):
|
|
533
|
+
continue
|
|
534
|
+
text = _read_text_safely(path, max_chars=1800).strip()
|
|
535
|
+
if not text:
|
|
536
|
+
continue
|
|
537
|
+
snippets.append(text)
|
|
538
|
+
evidence_paths.append(rel)
|
|
539
|
+
if len(snippets) >= 3:
|
|
540
|
+
break
|
|
541
|
+
return "\n\n".join(snippets).strip(), evidence_paths
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _has_meaningful_value(value: Any) -> bool:
|
|
545
|
+
if isinstance(value, str):
|
|
546
|
+
return bool(value.strip())
|
|
547
|
+
if isinstance(value, (list, dict, tuple, set)):
|
|
548
|
+
return bool(value)
|
|
549
|
+
return value is not None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _load_reference_doc(repo_root: Path, relative_path: str) -> str:
|
|
553
|
+
path = repo_root / relative_path
|
|
554
|
+
if not path.exists():
|
|
555
|
+
return ""
|
|
556
|
+
return path.read_text(encoding="utf-8")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _project_doc_reference_payload(repo_root: Path) -> dict[str, Any]:
|
|
562
|
+
references: dict[str, Any] = {}
|
|
563
|
+
for key, rel_path in PROJECT_DOC_REFERENCE_DOCS.items():
|
|
564
|
+
content = _load_reference_doc(repo_root, rel_path)
|
|
565
|
+
references[key] = {
|
|
566
|
+
"path": rel_path,
|
|
567
|
+
"content": content,
|
|
568
|
+
"loaded": bool(content.strip()),
|
|
569
|
+
}
|
|
570
|
+
return references
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _load_support_index(repo_root: Path, relative_path: str, *, expected_name: str) -> dict[str, Any]:
|
|
574
|
+
path = repo_root / relative_path
|
|
575
|
+
if not path.exists():
|
|
576
|
+
bundled_path = Path(__file__).resolve().parents[2] / relative_path
|
|
577
|
+
if bundled_path.exists():
|
|
578
|
+
path = bundled_path
|
|
579
|
+
payload = _load_json_dict(path, default={})
|
|
580
|
+
return {
|
|
581
|
+
"path": relative_path,
|
|
582
|
+
"loaded": path.exists() and bool(payload),
|
|
583
|
+
"classification": str(payload.get("classification") or "unknown"),
|
|
584
|
+
"contract": str(payload.get("contract") or ""),
|
|
585
|
+
"index_name": str(payload.get("index_name") or expected_name),
|
|
586
|
+
"payload": payload,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _support_indexes_payload(repo_root: Path) -> dict[str, Any]:
|
|
591
|
+
indexes = {name: _load_support_index(repo_root, rel_path, expected_name=name) for name, rel_path in SUPPORT_INDEX_DOCS.items()}
|
|
592
|
+
return {
|
|
593
|
+
"classification": "support_docs",
|
|
594
|
+
"indexes": indexes,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _compute_changed_sections(*, before_docs: dict[str, dict[str, Any]], after_docs: dict[str, dict[str, Any]], changed_docs: list[str]) -> list[str]:
|
|
599
|
+
changed_sections: list[str] = []
|
|
600
|
+
for doc_name in changed_docs:
|
|
601
|
+
before = before_docs.get(doc_name) if isinstance(before_docs.get(doc_name), dict) else {}
|
|
602
|
+
after = after_docs.get(doc_name) if isinstance(after_docs.get(doc_name), dict) else {}
|
|
603
|
+
section_keys = sorted(set(before.keys()) | set(after.keys()))
|
|
604
|
+
for section_key in section_keys:
|
|
605
|
+
if json.dumps(before.get(section_key), sort_keys=True) != json.dumps(after.get(section_key), sort_keys=True):
|
|
606
|
+
changed_sections.append(f"{doc_name}:{section_key}")
|
|
607
|
+
return changed_sections
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _project_doc_target_payloads(*, repo_root: Path, docs: dict[str, dict[str, Any]], changed_docs: list[str], changed_sections: list[str], support_indexes: dict[str, Any]) -> list[dict[str, Any]]:
|
|
611
|
+
cross_routes = (((support_indexes.get("indexes") or {}).get("cross_sourceXproject") or {}).get("payload") or {}).get("section_routes") or {}
|
|
612
|
+
project_entries = (((support_indexes.get("indexes") or {}).get("internal_project") or {}).get("payload") or {}).get("project_docs") or []
|
|
613
|
+
current_project_docs_root = _v2_project_docs_dir(repo_root) / "source_docs"
|
|
614
|
+
targets: list[dict[str, Any]] = []
|
|
615
|
+
for project_entry in project_entries:
|
|
616
|
+
if not isinstance(project_entry, dict):
|
|
617
|
+
continue
|
|
618
|
+
project_doc_name = str(project_entry.get("project_doc_name") or "").strip()
|
|
619
|
+
if not project_doc_name:
|
|
620
|
+
continue
|
|
621
|
+
primary_source_docs = _dedupe_str_list(list(project_entry.get("primary_source_docs") or []))
|
|
622
|
+
routed_sections = [section_ref for section_ref in changed_sections if project_doc_name in list(cross_routes.get(section_ref) or [])]
|
|
623
|
+
directly_impacted_by_changed_docs = _dedupe_str_list([section_ref.split(":", 1)[0] for section_ref in routed_sections])
|
|
624
|
+
supporting_source_docs = list(dict.fromkeys(primary_source_docs + directly_impacted_by_changed_docs))
|
|
625
|
+
existing_project_doc_path = current_project_docs_root / project_doc_name
|
|
626
|
+
existing_project_doc_content = existing_project_doc_path.read_text(encoding="utf-8") if existing_project_doc_path.exists() else ""
|
|
627
|
+
targets.append(
|
|
628
|
+
{
|
|
629
|
+
"project_doc_name": project_doc_name,
|
|
630
|
+
"primary_source_docs": primary_source_docs,
|
|
631
|
+
"supporting_source_docs": supporting_source_docs,
|
|
632
|
+
"directly_impacted_by_changed_docs": directly_impacted_by_changed_docs,
|
|
633
|
+
"routed_changed_sections": routed_sections,
|
|
634
|
+
"canonical_source_docs": {name: _clone(docs.get(name, {})) for name in supporting_source_docs},
|
|
635
|
+
"existing_project_doc": {
|
|
636
|
+
"path": str(existing_project_doc_path.relative_to(repo_root)) if existing_project_doc_path.exists() else str(existing_project_doc_path.relative_to(repo_root)),
|
|
637
|
+
"content": existing_project_doc_content,
|
|
638
|
+
"loaded": bool(existing_project_doc_content.strip()),
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
if not targets:
|
|
643
|
+
return []
|
|
644
|
+
fallback_targets: list[dict[str, Any]] = []
|
|
645
|
+
for item in targets:
|
|
646
|
+
if item["routed_changed_sections"]:
|
|
647
|
+
fallback_targets.append(item)
|
|
648
|
+
continue
|
|
649
|
+
if any(doc_name in changed_docs for doc_name in item["primary_source_docs"]):
|
|
650
|
+
copied = dict(item)
|
|
651
|
+
copied["directly_impacted_by_changed_docs"] = [doc_name for doc_name in item["primary_source_docs"] if doc_name in changed_docs]
|
|
652
|
+
fallback_targets.append(copied)
|
|
653
|
+
return fallback_targets
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _apply_explicit_source_doc_payload(
|
|
657
|
+
*, docs: dict[str, dict[str, Any]], explicit_payload: dict[str, Any], changed_docs: list[str]
|
|
658
|
+
) -> None:
|
|
659
|
+
for doc_name in CANONICAL_SOURCE_DOCS:
|
|
660
|
+
incoming = explicit_payload.get(doc_name)
|
|
661
|
+
if not isinstance(incoming, dict):
|
|
662
|
+
continue
|
|
663
|
+
merged = dict(docs[doc_name])
|
|
664
|
+
for key, value in incoming.items():
|
|
665
|
+
merged[key] = value
|
|
666
|
+
if json.dumps(merged, sort_keys=True) != json.dumps(docs[doc_name], sort_keys=True):
|
|
667
|
+
docs[doc_name] = merged
|
|
668
|
+
if doc_name not in changed_docs:
|
|
669
|
+
changed_docs.append(doc_name)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def build_source_doc_mutation_request(
|
|
673
|
+
*,
|
|
674
|
+
project_id: str,
|
|
675
|
+
idea_id: str,
|
|
676
|
+
repo_root: Path,
|
|
677
|
+
raw_text: str,
|
|
678
|
+
grounded_raw_text: str,
|
|
679
|
+
mode: str | None = None,
|
|
680
|
+
agent_runtime_mode: str | None = None,
|
|
681
|
+
history_grounding_ref: str | None = None,
|
|
682
|
+
existing_source_doc_refs: list[str] | None = None,
|
|
683
|
+
explicit_source_doc_payload: dict[str, Any] | None = None,
|
|
684
|
+
message_id: str | None = None,
|
|
685
|
+
session_id: str | None = None,
|
|
686
|
+
invoked_from: str = "idea_context_resolution",
|
|
687
|
+
pipeline_key: str | None = None,
|
|
688
|
+
) -> dict[str, Any]:
|
|
689
|
+
effective_mode = mode
|
|
690
|
+
if effective_mode is None:
|
|
691
|
+
effective_mode = "ideation_mutation" if (raw_text or grounded_raw_text or explicit_source_doc_payload) else "repo_bootstrap"
|
|
692
|
+
event = SourceDocMutationDagEvent(
|
|
693
|
+
project_id=project_id,
|
|
694
|
+
idea_id=idea_id,
|
|
695
|
+
repo_root=str(repo_root),
|
|
696
|
+
raw_text=raw_text,
|
|
697
|
+
grounded_raw_text=grounded_raw_text,
|
|
698
|
+
mode=effective_mode,
|
|
699
|
+
agent_runtime_mode=agent_runtime_mode,
|
|
700
|
+
history_grounding_ref=history_grounding_ref,
|
|
701
|
+
existing_source_doc_refs=existing_source_doc_refs or [],
|
|
702
|
+
explicit_source_doc_payload=explicit_source_doc_payload,
|
|
703
|
+
message_id=message_id,
|
|
704
|
+
session_id=session_id,
|
|
705
|
+
invoked_from=invoked_from,
|
|
706
|
+
pipeline_key=pipeline_key,
|
|
707
|
+
)
|
|
708
|
+
payload = event.model_dump()
|
|
709
|
+
if not payload.get("message_id"):
|
|
710
|
+
payload["message_id"] = _stable_id(
|
|
711
|
+
"msg_",
|
|
712
|
+
{"idea_id": idea_id, "raw_text": raw_text, "mode": payload.get("mode")},
|
|
713
|
+
size=16,
|
|
714
|
+
)
|
|
715
|
+
if not payload.get("pipeline_key"):
|
|
716
|
+
payload["pipeline_key"] = _stable_id(
|
|
717
|
+
"sdm_",
|
|
718
|
+
{
|
|
719
|
+
"idea_id": idea_id,
|
|
720
|
+
"project_id": project_id,
|
|
721
|
+
"raw_text": raw_text,
|
|
722
|
+
"grounded_raw_text": grounded_raw_text,
|
|
723
|
+
"mode": payload.get("mode"),
|
|
724
|
+
},
|
|
725
|
+
size=16,
|
|
726
|
+
)
|
|
727
|
+
return payload
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class SourceDocSectionArtifact(BaseModel):
|
|
731
|
+
explicit_facts: list[str] = []
|
|
732
|
+
assumed_facts: list[str] = []
|
|
733
|
+
rationale: list[str] = []
|
|
734
|
+
confidence_notes: list[str] = []
|
|
735
|
+
section_payload: dict[str, Any]
|
|
736
|
+
assumption_entries: list[dict[str, Any]] = []
|
|
737
|
+
cross_section_dependencies: list[str] = []
|
|
738
|
+
mode: str = "unknown"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class SourceDocCoherenceArtifact(BaseModel):
|
|
742
|
+
docs: dict[str, dict[str, Any]]
|
|
743
|
+
assumptions: list[dict[str, Any]]
|
|
744
|
+
notes: list[str] = []
|
|
745
|
+
mode: str = "unknown"
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class ProjectDocCoherenceArtifact(BaseModel):
|
|
749
|
+
notes: list[str] = []
|
|
750
|
+
mode: str = "unknown"
|
|
751
|
+
|
|
752
|
+
@field_validator("notes", mode="before")
|
|
753
|
+
@classmethod
|
|
754
|
+
def _normalize_notes(cls, value: Any) -> list[str]:
|
|
755
|
+
if value is None:
|
|
756
|
+
return []
|
|
757
|
+
if not isinstance(value, list):
|
|
758
|
+
value = [value]
|
|
759
|
+
out: list[str] = []
|
|
760
|
+
for item in value:
|
|
761
|
+
if isinstance(item, str):
|
|
762
|
+
text = item.strip()
|
|
763
|
+
elif isinstance(item, dict):
|
|
764
|
+
text = str(item.get("note") or item.get("summary") or item.get("detail") or json.dumps(item, sort_keys=True))
|
|
765
|
+
else:
|
|
766
|
+
text = str(item)
|
|
767
|
+
text = text.strip()
|
|
768
|
+
if text:
|
|
769
|
+
out.append(text)
|
|
770
|
+
return out
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _resolve_agent_runtime_mode(payload: dict[str, Any]) -> str:
|
|
774
|
+
requested = str(payload.get("agent_runtime_mode") or os.environ.get("DEVFLOW_SOURCE_DOC_AGENT_MODE") or "").strip().lower()
|
|
775
|
+
if requested:
|
|
776
|
+
if requested not in {"real", "stub"}:
|
|
777
|
+
raise RuntimeError(f"Unsupported source doc agent runtime mode: {requested!r}. Use 'real' or 'stub'.")
|
|
778
|
+
return requested
|
|
779
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
780
|
+
return "stub"
|
|
781
|
+
return "real"
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _record_agent_run(*, task_context: TaskContext, node_name: str, stage_name: str, runtime_mode: str, response_mode: str, prompt: dict[str, Any] | None = None, response: dict[str, Any] | None = None, raw_stdout: str = "", raw_stderr: str = "", error: str | None = None) -> None:
|
|
785
|
+
runs = task_context.metadata.setdefault("agent_runs", [])
|
|
786
|
+
runs.append({
|
|
787
|
+
"node_name": node_name,
|
|
788
|
+
"stage_name": stage_name,
|
|
789
|
+
"runtime_mode": runtime_mode,
|
|
790
|
+
"response_mode": response_mode,
|
|
791
|
+
"prompt": prompt,
|
|
792
|
+
"response": response,
|
|
793
|
+
"raw_stdout": raw_stdout,
|
|
794
|
+
"raw_stderr": raw_stderr,
|
|
795
|
+
"error": error,
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _mark_changed(changed_docs: list[str], name: str) -> None:
|
|
800
|
+
if name not in changed_docs:
|
|
801
|
+
changed_docs.append(name)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _contains_any(text: str, tokens: tuple[str, ...]) -> bool:
|
|
805
|
+
lowered = text.lower()
|
|
806
|
+
return any(token in lowered for token in tokens)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _infer_out_of_scope(raw_text: str) -> list[str]:
|
|
810
|
+
lowered = raw_text.lower()
|
|
811
|
+
exclusions: list[str] = []
|
|
812
|
+
if "task" in lowered:
|
|
813
|
+
exclusions.append("advanced workforce planning beyond task assignment and tracking")
|
|
814
|
+
if "dashboard" in lowered or "app" in lowered or "platform" in lowered:
|
|
815
|
+
exclusions.append("fully custom reporting outside the core workflow views")
|
|
816
|
+
return list(dict.fromkeys(exclusions))
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _infer_non_goals(raw_text: str) -> list[str]:
|
|
820
|
+
lowered = raw_text.lower()
|
|
821
|
+
non_goals: list[str] = []
|
|
822
|
+
if "task" in lowered:
|
|
823
|
+
non_goals.append("replace every surrounding back-office process in the first release")
|
|
824
|
+
if "staff" in lowered or "team" in lowered or "operator" in lowered:
|
|
825
|
+
non_goals.append("optimize for external customer self-service in the first release")
|
|
826
|
+
return list(dict.fromkeys(non_goals))
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _infer_constraints(raw_text: str) -> list[str]:
|
|
830
|
+
lowered = raw_text.lower()
|
|
831
|
+
constraints: list[str] = []
|
|
832
|
+
if "staff" in lowered or "team" in lowered or "operator" in lowered:
|
|
833
|
+
constraints.append("Permissions should keep assignment and status changes visible only to the relevant internal team roles.")
|
|
834
|
+
if "dashboard" in lowered:
|
|
835
|
+
constraints.append("The initial experience should fit a lightweight dashboard-first workflow rather than a complex multi-surface suite.")
|
|
836
|
+
if "task" in lowered:
|
|
837
|
+
constraints.append("The first release should keep the data model simple enough for dependable task creation, assignment, and status tracking.")
|
|
838
|
+
return list(dict.fromkeys(constraints))
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _infer_success_criteria(raw_text: str) -> list[str]:
|
|
842
|
+
lowered = raw_text.lower()
|
|
843
|
+
criteria: list[str] = []
|
|
844
|
+
if "task" in lowered:
|
|
845
|
+
criteria.append("Staff can create a task, assign an owner, and update status without leaving the product.")
|
|
846
|
+
if "assign" in lowered:
|
|
847
|
+
criteria.append("Managers can see who owns each task and which work is unassigned or overdue.")
|
|
848
|
+
if "dashboard" in lowered or "app" in lowered or "platform" in lowered:
|
|
849
|
+
criteria.append("The team can review the current work queue from a shared dashboard view.")
|
|
850
|
+
return list(dict.fromkeys(criteria))
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _derive_primary_actor(users: list[str], raw_text: str) -> str:
|
|
854
|
+
if users:
|
|
855
|
+
return users[0]
|
|
856
|
+
lowered = raw_text.lower()
|
|
857
|
+
if "staff" in lowered:
|
|
858
|
+
return "staff"
|
|
859
|
+
return "internal operators"
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _infer_primary_workflows(raw_text: str, users: list[str]) -> list[dict[str, Any]]:
|
|
863
|
+
lowered = raw_text.lower()
|
|
864
|
+
actor = _derive_primary_actor(users, raw_text)
|
|
865
|
+
workflows: list[dict[str, Any]] = []
|
|
866
|
+
if "task" in lowered:
|
|
867
|
+
workflows.append({
|
|
868
|
+
"workflow_key": "wf_capture_task",
|
|
869
|
+
"actor": actor,
|
|
870
|
+
"summary": "Capture a new task with enough context for the team to act on it.",
|
|
871
|
+
"trigger": "New work or follow-up request is identified.",
|
|
872
|
+
"steps": [
|
|
873
|
+
"Open the task app and create a new task record.",
|
|
874
|
+
"Add the core task details, priority, and any due-date context.",
|
|
875
|
+
"Save the task so it becomes visible to the team queue.",
|
|
876
|
+
],
|
|
877
|
+
"result": "A trackable task exists in the shared system.",
|
|
878
|
+
"status": "draft",
|
|
879
|
+
})
|
|
880
|
+
if ("task" in lowered or "assignment" in lowered or "assign" in lowered) and _contains_any(lowered, ("assign", "staff", "team", "operator")):
|
|
881
|
+
workflows.append({
|
|
882
|
+
"workflow_key": "wf_assign_and_track_task",
|
|
883
|
+
"actor": actor,
|
|
884
|
+
"summary": "Assign a task to the right staff member and track progress to completion.",
|
|
885
|
+
"trigger": "A task needs an owner or updated status.",
|
|
886
|
+
"steps": [
|
|
887
|
+
"Review the open task queue or dashboard.",
|
|
888
|
+
"Assign the task to a staff owner based on responsibility or availability.",
|
|
889
|
+
"Update task status as work starts, pauses, or completes.",
|
|
890
|
+
"Review overdue or blocked work and reassign if needed.",
|
|
891
|
+
],
|
|
892
|
+
"result": "Each active task has a visible owner and current status.",
|
|
893
|
+
"status": "draft",
|
|
894
|
+
})
|
|
895
|
+
return workflows
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _infer_alternate_flows(raw_text: str, users: list[str]) -> list[dict[str, Any]]:
|
|
899
|
+
lowered = raw_text.lower()
|
|
900
|
+
actor = _derive_primary_actor(users, raw_text)
|
|
901
|
+
alternates: list[dict[str, Any]] = []
|
|
902
|
+
if "task" in lowered and _contains_any(lowered, ("assign", "staff", "team", "operator")):
|
|
903
|
+
alternates.append({
|
|
904
|
+
"workflow_key": "wf_reassign_task",
|
|
905
|
+
"actor": actor,
|
|
906
|
+
"summary": "Reassign work when the original owner is unavailable or the task was routed incorrectly.",
|
|
907
|
+
"status": "draft",
|
|
908
|
+
})
|
|
909
|
+
if "due date" in lowered or "deadline" in lowered or "task" in lowered:
|
|
910
|
+
alternates.append({
|
|
911
|
+
"workflow_key": "wf_handle_overdue_task",
|
|
912
|
+
"actor": actor,
|
|
913
|
+
"summary": "Escalate or reprioritize overdue tasks so they do not disappear from the team queue.",
|
|
914
|
+
"status": "draft",
|
|
915
|
+
})
|
|
916
|
+
return alternates
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _infer_edge_cases(raw_text: str) -> list[str]:
|
|
920
|
+
lowered = raw_text.lower()
|
|
921
|
+
edge_cases: list[str] = []
|
|
922
|
+
if "task" in lowered:
|
|
923
|
+
edge_cases.extend([
|
|
924
|
+
"Tasks may be created without enough detail to assign confidently on the first pass.",
|
|
925
|
+
"Tasks can become blocked or overdue and need an escalation path.",
|
|
926
|
+
])
|
|
927
|
+
if "assign" in lowered or "staff" in lowered or "team" in lowered:
|
|
928
|
+
edge_cases.append("A task may need reassignment when the initial owner is unavailable or the wrong role picked it up.")
|
|
929
|
+
return list(dict.fromkeys(edge_cases))
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _infer_entity_bundle(raw_text: str, users: list[str]) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str]]:
|
|
933
|
+
lowered = raw_text.lower()
|
|
934
|
+
entities: list[dict[str, Any]] = []
|
|
935
|
+
relationships: list[dict[str, Any]] = []
|
|
936
|
+
ownership: list[str] = []
|
|
937
|
+
primary_actor = _derive_primary_actor(users, raw_text)
|
|
938
|
+
if "task" in lowered:
|
|
939
|
+
entities.append({
|
|
940
|
+
"entity_key": "task",
|
|
941
|
+
"label": "Task",
|
|
942
|
+
"purpose": "Represents a unit of work the team needs to capture, assign, and track through completion.",
|
|
943
|
+
"attributes": [
|
|
944
|
+
{"name": "title", "type": "string", "required": True, "notes": "Short operator-facing summary."},
|
|
945
|
+
{"name": "details", "type": "text", "required": False, "notes": "Working description or handoff context."},
|
|
946
|
+
{"name": "status", "type": "enum", "required": True, "notes": "Compact lifecycle state for visibility and reporting."},
|
|
947
|
+
{"name": "priority", "type": "enum", "required": False, "notes": "Simple urgency cue for triage."},
|
|
948
|
+
{"name": "due_date", "type": "datetime", "required": False, "notes": "Deadline used for queue ordering and overdue handling."},
|
|
949
|
+
],
|
|
950
|
+
"integrity_rules": [
|
|
951
|
+
"A task must have a title before it can be saved.",
|
|
952
|
+
"A completed task should retain a terminal status until intentionally reopened.",
|
|
953
|
+
],
|
|
954
|
+
"business_rules": [
|
|
955
|
+
"Each active task has at most one current assignee in the first release.",
|
|
956
|
+
"Tasks may exist unassigned immediately after capture.",
|
|
957
|
+
],
|
|
958
|
+
"authority_rules": [
|
|
959
|
+
"Task content and assignment changes are managed by internal team members, not external users.",
|
|
960
|
+
],
|
|
961
|
+
"lifecycle": ["created", "assigned", "in_progress", "completed"],
|
|
962
|
+
"workflow_links": ["wf_capture_task", "wf_assign_and_track_task"],
|
|
963
|
+
"unresolved_questions": [
|
|
964
|
+
"Whether blocked/cancelled states are needed beyond the compact default lifecycle.",
|
|
965
|
+
],
|
|
966
|
+
"status": "draft",
|
|
967
|
+
})
|
|
968
|
+
if ("task" in lowered or "assignment" in lowered or "assign" in lowered) and _contains_any(lowered, ("assign", "staff", "team", "operator")):
|
|
969
|
+
entities.append({
|
|
970
|
+
"entity_key": "staff_member",
|
|
971
|
+
"label": "Staff Member",
|
|
972
|
+
"purpose": "Represents an internal person who can own work and update task progress.",
|
|
973
|
+
"attributes": [
|
|
974
|
+
{"name": "display_name", "type": "string", "required": True, "notes": "Human-readable owner name."},
|
|
975
|
+
{"name": "role", "type": "string", "required": False, "notes": "Informational role/persona label until permissions are clarified."},
|
|
976
|
+
{"name": "availability_state", "type": "enum", "required": False, "notes": "Optional signal for assignment decisions."},
|
|
977
|
+
],
|
|
978
|
+
"integrity_rules": [
|
|
979
|
+
"An assignee reference should resolve to one active internal staff record.",
|
|
980
|
+
],
|
|
981
|
+
"business_rules": [
|
|
982
|
+
"Staff members can own many tasks over time.",
|
|
983
|
+
],
|
|
984
|
+
"authority_rules": [
|
|
985
|
+
"Staff identity data is maintained by the internal operating team or its delegated manager.",
|
|
986
|
+
],
|
|
987
|
+
"workflow_links": ["wf_assign_and_track_task"],
|
|
988
|
+
"unresolved_questions": [
|
|
989
|
+
"Whether roles remain informational or become permission-bearing in v1.",
|
|
990
|
+
],
|
|
991
|
+
"status": "draft",
|
|
992
|
+
})
|
|
993
|
+
ownership.append(f"{primary_actor} or a related team lead is assumed to control assignment decisions until role permissions are clarified.")
|
|
994
|
+
if "dashboard" in lowered or "queue" in lowered:
|
|
995
|
+
entities.append({
|
|
996
|
+
"entity_key": "task_queue_view",
|
|
997
|
+
"label": "Task Queue View",
|
|
998
|
+
"purpose": "Represents the shared operational view of open and in-progress work rather than a separate business record.",
|
|
999
|
+
"entity_kind": "derived_view",
|
|
1000
|
+
"attributes": [
|
|
1001
|
+
{"name": "filters", "type": "object", "required": False, "notes": "Simple filtering for status, owner, or urgency."},
|
|
1002
|
+
{"name": "sort_order", "type": "enum", "required": False, "notes": "Queue ordering such as due date or priority."},
|
|
1003
|
+
{"name": "status_groups", "type": "list", "required": False, "notes": "Compact groupings for dashboard visibility."},
|
|
1004
|
+
],
|
|
1005
|
+
"integrity_rules": [
|
|
1006
|
+
"The queue view is derived from task records and should not become the source of truth for task state.",
|
|
1007
|
+
],
|
|
1008
|
+
"business_rules": [
|
|
1009
|
+
"The initial release assumes one shared queue view is sufficient for the team.",
|
|
1010
|
+
],
|
|
1011
|
+
"authority_rules": [
|
|
1012
|
+
"View configuration may be adjustable, but task records remain authoritative for workflow state.",
|
|
1013
|
+
],
|
|
1014
|
+
"workflow_links": ["wf_assign_and_track_task"],
|
|
1015
|
+
"unresolved_questions": [
|
|
1016
|
+
"Whether multiple team-specific queue views are needed after the first release.",
|
|
1017
|
+
],
|
|
1018
|
+
"status": "draft",
|
|
1019
|
+
})
|
|
1020
|
+
entity_keys = {item["entity_key"] for item in entities}
|
|
1021
|
+
if {"task", "staff_member"}.issubset(entity_keys):
|
|
1022
|
+
relationships.append({
|
|
1023
|
+
"relationship_key": "task_current_assignee",
|
|
1024
|
+
"relationship_type": "assignment",
|
|
1025
|
+
"from_entity": "task",
|
|
1026
|
+
"to_entity": "staff_member",
|
|
1027
|
+
"cardinality": "many_to_one",
|
|
1028
|
+
"summary": "Each active task may reference one current owner, while a staff member can own many tasks.",
|
|
1029
|
+
"integrity_rules": [
|
|
1030
|
+
"A task cannot reference more than one current assignee at the same time.",
|
|
1031
|
+
],
|
|
1032
|
+
"business_rules": [
|
|
1033
|
+
"Assignment may change over time without creating a new task record.",
|
|
1034
|
+
],
|
|
1035
|
+
"authority_rules": [
|
|
1036
|
+
"Assignment changes are controlled by internal operators or managers until a stricter RBAC model is confirmed.",
|
|
1037
|
+
],
|
|
1038
|
+
"unresolved_questions": [
|
|
1039
|
+
"Whether reassignment history needs its own event log in the first release.",
|
|
1040
|
+
],
|
|
1041
|
+
"status": "draft",
|
|
1042
|
+
})
|
|
1043
|
+
if {"task", "task_queue_view"}.issubset(entity_keys):
|
|
1044
|
+
relationships.append({
|
|
1045
|
+
"relationship_key": "task_visible_in_queue_view",
|
|
1046
|
+
"relationship_type": "projection",
|
|
1047
|
+
"from_entity": "task",
|
|
1048
|
+
"to_entity": "task_queue_view",
|
|
1049
|
+
"cardinality": "many_to_many",
|
|
1050
|
+
"summary": "Open and in-progress tasks appear in the shared queue/dashboard used for triage and tracking.",
|
|
1051
|
+
"integrity_rules": [
|
|
1052
|
+
"Queue membership is derived from task state rather than stored as an independent authoritative record.",
|
|
1053
|
+
],
|
|
1054
|
+
"business_rules": [
|
|
1055
|
+
"Closed tasks may drop out of the default queue while remaining searchable elsewhere.",
|
|
1056
|
+
],
|
|
1057
|
+
"authority_rules": [
|
|
1058
|
+
"Queue visibility rules should follow task visibility and team permissions.",
|
|
1059
|
+
],
|
|
1060
|
+
"unresolved_questions": [
|
|
1061
|
+
"Whether completed work stays visible by default or moves to a historical view.",
|
|
1062
|
+
],
|
|
1063
|
+
"status": "draft",
|
|
1064
|
+
})
|
|
1065
|
+
if not ownership and entities:
|
|
1066
|
+
ownership.append("One internal team is assumed to own task and staff assignment records until a fuller authority model is confirmed.")
|
|
1067
|
+
return entities, relationships, ownership
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _enrich_sparse_source_docs(*, grounded_text: str, docs: dict[str, dict[str, Any]], assumptions: list[dict[str, Any]], payload: dict[str, Any], requested_mode: str) -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]], list[str]]:
|
|
1071
|
+
if requested_mode != "ideation_mutation":
|
|
1072
|
+
return docs, assumptions, []
|
|
1073
|
+
notes: list[str] = []
|
|
1074
|
+
product = docs["product_brief.json"]
|
|
1075
|
+
workflows = docs["user_workflows.json"]
|
|
1076
|
+
entities = docs["domain_entities.json"]
|
|
1077
|
+
if not isinstance(product.get("scope"), dict):
|
|
1078
|
+
product["scope"] = {}
|
|
1079
|
+
users = _dedupe_str_list(list(product.get("target_users") or []) + list(workflows.get("actors") or []) + _infer_users(grounded_text))
|
|
1080
|
+
if not product.get("target_users") and users:
|
|
1081
|
+
product["target_users"] = users
|
|
1082
|
+
notes.append("Aligned target users from workflow actors/inferred sparse intake roles.")
|
|
1083
|
+
if not workflows.get("actors") and users:
|
|
1084
|
+
workflows["actors"] = users
|
|
1085
|
+
notes.append("Filled workflow actors from the reconciled target-user set.")
|
|
1086
|
+
product["scope"]["out_of_scope"] = _dedupe_str_list(list((product.get("scope") or {}).get("out_of_scope") or []) + _infer_out_of_scope(grounded_text))
|
|
1087
|
+
product["non_goals"] = _dedupe_str_list(list(product.get("non_goals") or []) + _infer_non_goals(grounded_text))
|
|
1088
|
+
product["constraints"] = _dedupe_str_list(list(product.get("constraints") or []) + _infer_constraints(grounded_text))
|
|
1089
|
+
product["success_criteria"] = _dedupe_str_list(list(product.get("success_criteria") or []) + _infer_success_criteria(grounded_text))
|
|
1090
|
+
primary_workflows = list(workflows.get("primary_workflows") or [])
|
|
1091
|
+
if not primary_workflows:
|
|
1092
|
+
workflows["primary_workflows"] = _infer_primary_workflows(grounded_text, users)
|
|
1093
|
+
if workflows["primary_workflows"]:
|
|
1094
|
+
notes.append("Added compact primary workflows so sparse source docs still preserve trigger/steps/result shape.")
|
|
1095
|
+
workflows["alternate_flows"] = list(workflows.get("alternate_flows") or [])
|
|
1096
|
+
if not workflows["alternate_flows"]:
|
|
1097
|
+
workflows["alternate_flows"] = _infer_alternate_flows(grounded_text, users)
|
|
1098
|
+
workflows["edge_cases"] = _dedupe_str_list(list(workflows.get("edge_cases") or []) + _infer_edge_cases(grounded_text))
|
|
1099
|
+
workflow_assumptions = _dedupe_str_list(list(workflows.get("workflow_assumptions") or []))
|
|
1100
|
+
if not workflow_assumptions and workflows["primary_workflows"] and _contains_any(grounded_text, ("task", "assign", "workflow", "queue")):
|
|
1101
|
+
workflow_assumptions.append("The initial workflow model assumes one primary work-capture path with only the most likely operational branches filled in.")
|
|
1102
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/user_workflows.json", section_key="workflow_assumptions", topic_key="default_primary_workflow", current_value=workflow_assumptions[0], reasoning="Sparse intake still implies at least one end-to-end operational path.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1103
|
+
if workflows["alternate_flows"] and workflow_assumptions and len(workflow_assumptions) == 1:
|
|
1104
|
+
workflow_assumptions.append("Alternate flows stay limited to reassignment and overdue handling until more workflow policy is known.")
|
|
1105
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/user_workflows.json", section_key="workflow_assumptions", topic_key="sparse_workflow_policy", current_value="Alternate flows are limited to reassignment and overdue handling until clarified.", reasoning="The request implies task operations but not a full exception catalog.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1106
|
+
workflows["workflow_assumptions"] = workflow_assumptions
|
|
1107
|
+
inferred_entities, inferred_relationships, inferred_ownership = _infer_entity_bundle(grounded_text, users)
|
|
1108
|
+
entities_were_sparse = not bool(list(entities.get("entities") or []))
|
|
1109
|
+
if entities_were_sparse:
|
|
1110
|
+
entities["entities"] = inferred_entities
|
|
1111
|
+
if inferred_entities:
|
|
1112
|
+
notes.append("Inferred compact domain entities from sparse workflow verbs and actor roles.")
|
|
1113
|
+
if not entities.get("relationships"):
|
|
1114
|
+
entities["relationships"] = inferred_relationships
|
|
1115
|
+
if inferred_relationships:
|
|
1116
|
+
notes.append("Added entity relationships needed to keep workflows and architecture grounding coherent.")
|
|
1117
|
+
entities["ownership_assumptions"] = _dedupe_str_list(list(entities.get("ownership_assumptions") or []) + inferred_ownership)
|
|
1118
|
+
if inferred_entities and entities_were_sparse:
|
|
1119
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/domain_entities.json", section_key="ownership_assumptions", topic_key="entity_authority", current_value=entities["ownership_assumptions"][0] if entities["ownership_assumptions"] else "One internal team maintains task and assignment state until record authority is clarified.", reasoning="Sparse intake implies ownership for task and assignment data, but not the final authority model.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1120
|
+
return docs, assumptions, notes
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _heuristic_section_payloads(*, grounded_text: str, docs: dict[str, dict[str, Any]], assumptions: list[dict[str, Any]], payload: dict[str, Any], requested_mode: str, effective_mode: str) -> dict[str, SourceDocSectionArtifact]:
|
|
1124
|
+
base_product = dict(docs["product_brief.json"])
|
|
1125
|
+
base_workflows = dict(docs["user_workflows.json"])
|
|
1126
|
+
base_entities = dict(docs["domain_entities.json"])
|
|
1127
|
+
product_assumptions = _dedupe_str_list(list(base_product.get("assumptions") or []))
|
|
1128
|
+
workflow_assumptions = _dedupe_str_list(list(base_workflows.get("workflow_assumptions") or []))
|
|
1129
|
+
ownership_assumptions = _dedupe_str_list(list(base_entities.get("ownership_assumptions") or []))
|
|
1130
|
+
if effective_mode != "scaffold_only":
|
|
1131
|
+
sufficient_idea = extract_sufficient_idea(grounded_text)
|
|
1132
|
+
users = _dedupe_str_list(list(base_product.get("target_users") or []) + list(sufficient_idea.get("target_users") or []) + _infer_users(grounded_text))
|
|
1133
|
+
product_summary = _derive_product_summary(grounded_text, sufficient_idea)
|
|
1134
|
+
if product_summary:
|
|
1135
|
+
base_product["product_summary"] = product_summary
|
|
1136
|
+
problem = str(sufficient_idea.get("problem") or _infer_problem(grounded_text) or base_product.get("problem_statement") or "").strip()
|
|
1137
|
+
if problem:
|
|
1138
|
+
base_product["problem_statement"] = problem
|
|
1139
|
+
elif requested_mode == "ideation_mutation":
|
|
1140
|
+
inferred_problem = "The team is handling work through an unclear or manual process, making ownership and progress hard to track."
|
|
1141
|
+
base_product["problem_statement"] = inferred_problem
|
|
1142
|
+
product_assumptions.append("The current workflow likely relies on manual tracking or fragmented tools.")
|
|
1143
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="problem_statement", topic_key="primary_problem", current_value=inferred_problem, reasoning="The intake asks for software support but does not spell out the current-state pain in detail.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1144
|
+
base_product["target_users"] = users
|
|
1145
|
+
base_workflows["actors"] = _dedupe_str_list(list(base_workflows.get("actors") or []) + users)
|
|
1146
|
+
goals = _dedupe_str_list(list(base_product.get("goals") or []) + [sufficient_idea.get("goal") or _infer_goal(grounded_text)])
|
|
1147
|
+
if not goals and requested_mode == "ideation_mutation" and _contains_any(grounded_text, ("task", "assign", "workflow", "queue", "dashboard")):
|
|
1148
|
+
goals = ["Create a lightweight shared workflow so the team can capture, assign, and complete work reliably."]
|
|
1149
|
+
product_assumptions.append("The first release should improve day-to-day operational visibility more than advanced analytics.")
|
|
1150
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="goals", topic_key="primary_goal", current_value=goals[0], reasoning="Sparse software requests still imply an operational outcome even when the target metric is unstated.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1151
|
+
base_product["goals"] = goals
|
|
1152
|
+
scope = dict(base_product.get("scope") or {"in_scope": [], "out_of_scope": []})
|
|
1153
|
+
scope["in_scope"] = _dedupe_str_list(list(scope.get("in_scope") or []) + list(sufficient_idea.get("scope") or []) + _infer_scope_bullets(grounded_text))
|
|
1154
|
+
base_product["scope"] = scope
|
|
1155
|
+
if requested_mode == "ideation_mutation":
|
|
1156
|
+
if not users:
|
|
1157
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="target_users", topic_key="primary_users", current_value=["internal operators"], reasoning="Software-delivery intake typically targets an operator or requester cohort even when the exact role is missing.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1158
|
+
product_assumptions.append("Primary users are likely internal operators until clarified.")
|
|
1159
|
+
if not scope["in_scope"]:
|
|
1160
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="scope", topic_key="initial_scope", current_value=["first build should focus on the smallest workflow that resolves the request"], reasoning="The message asks for software work but does not yet define a narrow first release scope.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1161
|
+
product_assumptions.append("Initial scope should stay tightly limited until the first release boundaries are confirmed.")
|
|
1162
|
+
base_product["scope"]["out_of_scope"] = _dedupe_str_list(list(scope.get("out_of_scope") or []) + _infer_out_of_scope(grounded_text))
|
|
1163
|
+
base_product["non_goals"] = _dedupe_str_list(list(base_product.get("non_goals") or []) + _infer_non_goals(grounded_text))
|
|
1164
|
+
base_product["constraints"] = _dedupe_str_list(list(base_product.get("constraints") or []) + list(sufficient_idea.get("constraints") or []) + _infer_constraints(grounded_text))
|
|
1165
|
+
base_product["success_criteria"] = _dedupe_str_list(list(base_product.get("success_criteria") or []) + list(sufficient_idea.get("acceptance_criteria") or []) + _infer_success_criteria(grounded_text))
|
|
1166
|
+
base_product["assumptions"] = _dedupe_str_list(product_assumptions)
|
|
1167
|
+
|
|
1168
|
+
inferred_primary_workflows = _infer_primary_workflows(grounded_text, users)
|
|
1169
|
+
inferred_alternates = _infer_alternate_flows(grounded_text, users)
|
|
1170
|
+
had_primary_workflows = bool(list(base_workflows.get("primary_workflows") or []))
|
|
1171
|
+
base_workflows["primary_workflows"] = list(base_workflows.get("primary_workflows") or []) or inferred_primary_workflows
|
|
1172
|
+
base_workflows["alternate_flows"] = list(base_workflows.get("alternate_flows") or []) or inferred_alternates
|
|
1173
|
+
base_workflows["edge_cases"] = _dedupe_str_list(list(base_workflows.get("edge_cases") or []) + _infer_edge_cases(grounded_text))
|
|
1174
|
+
if (not had_primary_workflows) and inferred_primary_workflows and not workflow_assumptions and requested_mode == "ideation_mutation":
|
|
1175
|
+
workflow_assumptions.append("The first release centers on one capture-and-assignment workflow with only the most important exception paths filled in.")
|
|
1176
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/user_workflows.json", section_key="workflow_assumptions", topic_key="sparse_workflow_model", current_value=workflow_assumptions[0], reasoning="Sparse intake names the problem shape but not the complete operating playbook.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1177
|
+
base_workflows["workflow_assumptions"] = _dedupe_str_list(workflow_assumptions)
|
|
1178
|
+
|
|
1179
|
+
inferred_entities, inferred_relationships, inferred_ownership = _infer_entity_bundle(grounded_text, users)
|
|
1180
|
+
base_entities["entities"] = list(base_entities.get("entities") or []) or inferred_entities
|
|
1181
|
+
base_entities["relationships"] = list(base_entities.get("relationships") or []) or inferred_relationships
|
|
1182
|
+
ownership_assumptions = _dedupe_str_list(ownership_assumptions + inferred_ownership)
|
|
1183
|
+
base_entities["ownership_assumptions"] = ownership_assumptions
|
|
1184
|
+
if inferred_entities and requested_mode == "ideation_mutation":
|
|
1185
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/domain_entities.json", section_key="ownership_assumptions", topic_key="entity_authority", current_value=ownership_assumptions[0] if ownership_assumptions else "One internal team maintains task and assignment state until record authority is clarified.", reasoning="Sparse intake implies a compact operating model but does not define final data authority boundaries.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1186
|
+
return {
|
|
1187
|
+
"product_brief": SourceDocSectionArtifact(section_payload=base_product, explicit_facts=[f for f in [base_product.get("product_summary"), base_product.get("problem_statement")] if f], assumed_facts=list(base_product.get("assumptions") or []), rationale=["Populate the product brief to a canonically sufficient planning shape even when intake is sparse."], confidence_notes=["Sparse-input heuristics keep the brief compact and assumption-aware rather than pseudo-PRD level."], mode="heuristic"),
|
|
1188
|
+
"user_workflows": SourceDocSectionArtifact(section_payload=base_workflows, explicit_facts=list(base_workflows.get("actors") or []), assumed_facts=list(base_workflows.get("workflow_assumptions") or []), rationale=["Preserve at least one end-to-end workflow shape so downstream PRD and UX docs have grounded flow structure."], confidence_notes=["Workflow depth is intentionally compact and should not expand into full story decomposition here."], mode="heuristic"),
|
|
1189
|
+
"domain_entities": SourceDocSectionArtifact(section_payload=base_entities, explicit_facts=[str(item.get("label") or item.get("entity_key") or "") for item in list(base_entities.get("entities") or []) if isinstance(item, dict)], assumed_facts=list(base_entities.get("ownership_assumptions") or []), rationale=["Infer only the minimum business objects and relationships required to keep sparse workflows coherent."], confidence_notes=["Entity inference stays compact and avoids turning source docs into pseudo-architecture documents."], mode="heuristic"),
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _run_source_doc_agent(*, repo_root: Path, node_name: str, stage_name: str, output_model: type[BaseModel], context_payload: dict[str, Any], guidance: list[str], fallback: BaseModel, runtime_mode: str, task_context: TaskContext) -> tuple[BaseModel, str]:
|
|
1194
|
+
if runtime_mode == "stub":
|
|
1195
|
+
_record_agent_run(task_context=task_context, node_name=node_name, stage_name=stage_name, runtime_mode=runtime_mode, response_mode="test_stub", response=fallback.model_dump() if hasattr(fallback, "model_dump") else None)
|
|
1196
|
+
return fallback, "test_stub"
|
|
1197
|
+
model, envelope = run_agent_step(repo_root=repo_root, stage_name=f"source_doc_mutation_{stage_name}", output_model=output_model, context_payload=context_payload, guidance=guidance)
|
|
1198
|
+
_record_agent_run(task_context=task_context, node_name=node_name, stage_name=stage_name, runtime_mode=runtime_mode, response_mode="agent", prompt=envelope.prompt, response=envelope.response, raw_stdout=envelope.raw_stdout, raw_stderr=envelope.raw_stderr)
|
|
1199
|
+
return model, "agent"
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def _normalize_text_tokens(text: str) -> list[str]:
|
|
1203
|
+
return re.findall(r"[a-z0-9_]+", text.lower())
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
_STOPWORDS = {
|
|
1207
|
+
"the", "and", "for", "with", "that", "this", "from", "into", "your", "their", "then", "than", "when", "where", "while", "must", "should", "would", "could", "have", "has", "had", "was", "were", "are", "our", "out", "use", "using", "used", "one", "two", "three", "same", "each", "only", "also", "does", "did", "done", "not", "but", "can", "all", "any", "per", "via", "its", "it's", "it", "a", "an", "of", "to", "in", "on", "by", "or", "as", "at", "be", "is",
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _content_keyword_set(value: Any) -> set[str]:
|
|
1212
|
+
if isinstance(value, dict):
|
|
1213
|
+
out: set[str] = set()
|
|
1214
|
+
for key, item in value.items():
|
|
1215
|
+
out.update(_content_keyword_set(key))
|
|
1216
|
+
out.update(_content_keyword_set(item))
|
|
1217
|
+
return out
|
|
1218
|
+
if isinstance(value, list):
|
|
1219
|
+
out: set[str] = set()
|
|
1220
|
+
for item in value:
|
|
1221
|
+
out.update(_content_keyword_set(item))
|
|
1222
|
+
return out
|
|
1223
|
+
if not isinstance(value, str):
|
|
1224
|
+
return set()
|
|
1225
|
+
return {token for token in _normalize_text_tokens(value) if len(token) >= 4 and token not in _STOPWORDS}
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _project_doc_grounding_issues(*, project_doc_name: str, content: str, target_payload: dict[str, Any]) -> list[str]:
|
|
1229
|
+
if project_doc_name != "prd.md":
|
|
1230
|
+
return []
|
|
1231
|
+
canonical_source_docs = dict(target_payload.get("canonical_source_docs") or {})
|
|
1232
|
+
existing_project_doc = dict(target_payload.get("existing_project_doc") or {})
|
|
1233
|
+
output_keywords = _content_keyword_set(content)
|
|
1234
|
+
source_keywords = _content_keyword_set(canonical_source_docs)
|
|
1235
|
+
baseline_keywords = _content_keyword_set(str(existing_project_doc.get("content") or ""))
|
|
1236
|
+
actor_keywords = _content_keyword_set((canonical_source_docs.get("user_workflows.json") or {}).get("actors") or [])
|
|
1237
|
+
entity_keywords = _content_keyword_set((canonical_source_docs.get("domain_entities.json") or {}).get("entities") or [])
|
|
1238
|
+
workflow_keywords = _content_keyword_set((canonical_source_docs.get("user_workflows.json") or {}).get("primary_workflows") or [])
|
|
1239
|
+
required_keywords = {token for token in actor_keywords | entity_keywords | workflow_keywords if len(token) >= 4}
|
|
1240
|
+
issues: list[str] = []
|
|
1241
|
+
if required_keywords and not (output_keywords & required_keywords):
|
|
1242
|
+
issues.append("PRD lost all grounded actor/entity/workflow anchors from the canonical source docs.")
|
|
1243
|
+
baseline_anchor_keywords = {token for token in baseline_keywords if token in source_keywords and len(token) >= 4}
|
|
1244
|
+
if baseline_anchor_keywords and len(output_keywords & baseline_anchor_keywords) < min(3, len(baseline_anchor_keywords)):
|
|
1245
|
+
issues.append("PRD does not preserve enough baseline product identity anchors from the existing PRD/source-doc overlap.")
|
|
1246
|
+
product_summary_keywords = _content_keyword_set(str((canonical_source_docs.get("product_brief.json") or {}).get("product_summary") or ""))
|
|
1247
|
+
if product_summary_keywords and not (output_keywords & product_summary_keywords):
|
|
1248
|
+
issues.append("PRD is not recognizably grounded in the current product summary.")
|
|
1249
|
+
return issues
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _run_project_doc_subagent_cli(*, repo_root: str, output_path: str, project_doc_name: str, target_payload: dict[str, Any], shell_only: bool, runtime_mode: str) -> None:
|
|
1253
|
+
path = Path(output_path)
|
|
1254
|
+
canonical_source_docs = dict(target_payload.get("canonical_source_docs") or {})
|
|
1255
|
+
supporting_source_docs = list(target_payload.get("supporting_source_docs") or [])
|
|
1256
|
+
primary_source_docs = list(target_payload.get("primary_source_docs") or [])
|
|
1257
|
+
reference_docs = dict(target_payload.get("reference_docs") or {})
|
|
1258
|
+
direct_impacts = list(target_payload.get("directly_impacted_by_changed_docs") or [])
|
|
1259
|
+
existing_project_doc = dict(target_payload.get("existing_project_doc") or {})
|
|
1260
|
+
if runtime_mode == "stub":
|
|
1261
|
+
rendered = "\n".join([
|
|
1262
|
+
f"# {project_doc_name.replace('_', ' ').replace('.md', '').title()}",
|
|
1263
|
+
"",
|
|
1264
|
+
f"Grounded in: {', '.join(supporting_source_docs) or 'none'}.",
|
|
1265
|
+
f"Primary sources: {', '.join(primary_source_docs) or 'none'}.",
|
|
1266
|
+
f"Direct impacts: {', '.join(direct_impacts) or 'none'}.",
|
|
1267
|
+
f"Existing baseline doc loaded: {'yes' if existing_project_doc.get('loaded') else 'no'}.",
|
|
1268
|
+
"",
|
|
1269
|
+
"## Grounding Notes",
|
|
1270
|
+
"",
|
|
1271
|
+
"- Project-doc richness and cross-reference contracts were supplied.",
|
|
1272
|
+
"- This stub output stays thin by design; richer prose requires the real agent runtime.",
|
|
1273
|
+
"",
|
|
1274
|
+
"## Canonical Source Snapshot",
|
|
1275
|
+
"",
|
|
1276
|
+
"```json",
|
|
1277
|
+
json.dumps(canonical_source_docs, indent=2, sort_keys=True),
|
|
1278
|
+
"```",
|
|
1279
|
+
"",
|
|
1280
|
+
])
|
|
1281
|
+
_write_json(path, {"project_doc_name": project_doc_name, "status": "completed", "mode": "test_stub_subagent", "runtime_mode": runtime_mode, "content": rendered, "source_doc_dependencies": supporting_source_docs, "primary_source_docs": primary_source_docs, "impact_rationale": direct_impacts, "reference_docs_supplied": {key: value.get('path') for key, value in reference_docs.items() if isinstance(value, dict)}, "confidence_notes": ["Stub runtime cannot demonstrate full richness/depth."]})
|
|
1282
|
+
return
|
|
1283
|
+
class _ProjectDocArtifact(BaseModel):
|
|
1284
|
+
content: str
|
|
1285
|
+
source_doc_dependencies: list[str] = []
|
|
1286
|
+
assumptions_used: list[str] = []
|
|
1287
|
+
impact_rationale: list[str] = []
|
|
1288
|
+
confidence_notes: list[str] = []
|
|
1289
|
+
|
|
1290
|
+
base_guidance = load_agentic_prompt_lines("source_doc_mutation_project_doc_render")
|
|
1291
|
+
attempts: list[dict[str, Any]] = []
|
|
1292
|
+
last_model = None
|
|
1293
|
+
last_envelope = None
|
|
1294
|
+
for attempt in range(2):
|
|
1295
|
+
guidance = list(base_guidance)
|
|
1296
|
+
if attempt == 1 and attempts:
|
|
1297
|
+
guidance.append("Your previous draft failed grounding validation. Repair it instead of changing products.")
|
|
1298
|
+
guidance.extend([f"Validation failure: {issue}" for issue in attempts[-1].get("issues") or []])
|
|
1299
|
+
model, envelope = run_agent_step(repo_root=Path(repo_root), stage_name=f"project_doc_mutation_{project_doc_name.replace('.', '_')}", output_model=_ProjectDocArtifact, context_payload={"project_doc_name": project_doc_name, "target_payload": target_payload, "shell_only": shell_only}, guidance=guidance)
|
|
1300
|
+
issues = _project_doc_grounding_issues(project_doc_name=project_doc_name, content=model.content, target_payload=target_payload)
|
|
1301
|
+
attempts.append({"attempt": attempt + 1, "issues": issues, "prompt": envelope.prompt, "response": envelope.response})
|
|
1302
|
+
last_model = model
|
|
1303
|
+
last_envelope = envelope
|
|
1304
|
+
if not issues:
|
|
1305
|
+
break
|
|
1306
|
+
final_issues = _project_doc_grounding_issues(project_doc_name=project_doc_name, content=last_model.content if last_model else "", target_payload=target_payload)
|
|
1307
|
+
status = "completed" if not final_issues else "failed_validation"
|
|
1308
|
+
_write_json(path, {"project_doc_name": project_doc_name, "status": status, "mode": "agent_subprocess", "runtime_mode": runtime_mode, "content": last_model.content if last_model else "", "source_doc_dependencies": last_model.source_doc_dependencies if last_model else [], "assumptions_used": last_model.assumptions_used if last_model else [], "impact_rationale": last_model.impact_rationale if last_model else [], "confidence_notes": (last_model.confidence_notes if last_model else []) + ([f"Grounding validation failed: {issue}" for issue in final_issues] if final_issues else []), "primary_source_docs": primary_source_docs, "reference_docs_supplied": {key: value.get('path') for key, value in reference_docs.items() if isinstance(value, dict)}, "existing_project_doc_supplied": bool(existing_project_doc.get("loaded")), "validation_issues": final_issues, "attempts": attempts, "prompt": last_envelope.prompt if last_envelope else {}, "response": last_envelope.response if last_envelope else {}, "raw_stdout": last_envelope.raw_stdout if last_envelope else "", "raw_stderr": last_envelope.raw_stderr if last_envelope else ""})
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
class NormalizeAndGatherSupportContextNode(Node):
|
|
1312
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1313
|
+
payload = build_source_doc_mutation_request(project_id=str(task_context.event.project_id or ""), idea_id=str(task_context.event.idea_id or ""), repo_root=Path(str(task_context.event.repo_root or "")), raw_text=str(task_context.event.raw_text or ""), grounded_raw_text=str(task_context.event.grounded_raw_text or task_context.event.raw_text or ""), mode=str(task_context.event.mode or "").strip() or None, history_grounding_ref=task_context.event.history_grounding_ref, existing_source_doc_refs=list(task_context.event.existing_source_doc_refs or []), explicit_source_doc_payload=task_context.event.explicit_source_doc_payload, message_id=task_context.event.message_id, session_id=task_context.event.session_id, invoked_from=str(task_context.event.invoked_from or "idea_context_resolution"), pipeline_key=task_context.event.pipeline_key)
|
|
1314
|
+
repo_root = Path(payload["repo_root"]).expanduser().resolve()
|
|
1315
|
+
pipeline_key = str(payload["pipeline_key"])
|
|
1316
|
+
pipeline_dir = _pipeline_root(repo_root, idea_id=str(payload["idea_id"]), pipeline_key=pipeline_key)
|
|
1317
|
+
pipeline_dir.mkdir(parents=True, exist_ok=True)
|
|
1318
|
+
run_id = f"{DAG_ID}:{pipeline_key}"
|
|
1319
|
+
docs = _load_source_doc_state(repo_root)
|
|
1320
|
+
source_docs_before = _clone(docs)
|
|
1321
|
+
registry_before = [item for item in list((docs.get("assumptions_registry.json") or {}).get("assumptions") or []) if isinstance(item, dict)]
|
|
1322
|
+
repo_inventory = _repo_bootstrap_inventory(repo_root)
|
|
1323
|
+
requested_mode = str(payload.get("mode") or "ideation_mutation")
|
|
1324
|
+
agent_runtime_mode = _resolve_agent_runtime_mode(payload)
|
|
1325
|
+
grounded_text = str(payload.get("grounded_raw_text") or payload.get("raw_text") or "")
|
|
1326
|
+
repo_grounding_text, repo_grounding_refs = ("", [])
|
|
1327
|
+
effective_mode = requested_mode
|
|
1328
|
+
trust_level = "source_docs_mutated"
|
|
1329
|
+
mutation_origin = "message_grounded"
|
|
1330
|
+
plan_notes: list[str] = []
|
|
1331
|
+
shell_only = False
|
|
1332
|
+
if requested_mode == "repo_bootstrap":
|
|
1333
|
+
repo_grounding_text, repo_grounding_refs = _pick_repo_grounding_text(repo_root)
|
|
1334
|
+
if repo_inventory["has_genuine_signal"] and repo_grounding_text:
|
|
1335
|
+
grounded_text = repo_grounding_text
|
|
1336
|
+
mutation_origin = "repo_grounded_bootstrap"
|
|
1337
|
+
plan_notes.append("Repo evidence cleared bootstrap threshold; grounding source docs from repo files.")
|
|
1338
|
+
else:
|
|
1339
|
+
effective_mode = "scaffold_only"
|
|
1340
|
+
trust_level = "no_source_doc_changes"
|
|
1341
|
+
mutation_origin = "scaffold_only_fallback"
|
|
1342
|
+
shell_only = True
|
|
1343
|
+
plan_notes.append("Repo evidence too sparse for grounded bootstrap; emitting scaffold-only docs and derived shells.")
|
|
1344
|
+
inventory = {"requested_mode": requested_mode, "repo_root": str(repo_root), "canonical_source_docs_root": str(_source_docs_dir(repo_root)), "derived_project_docs_root": str(_v2_project_docs_dir(repo_root)), "existing_source_doc_refs": list(payload.get("existing_source_doc_refs") or []), "source_docs_before": source_docs_before, "preexisting_nonempty_docs": sorted([name for name, doc in source_docs_before.items() if isinstance(doc, dict) and any(_has_meaningful_value(v) for v in doc.values())]), "repo_inventory": repo_inventory}
|
|
1345
|
+
changed_docs: list[str] = []
|
|
1346
|
+
_apply_explicit_source_doc_payload(docs=docs, explicit_payload=payload.get("explicit_source_doc_payload") if isinstance(payload.get("explicit_source_doc_payload"), dict) else {}, changed_docs=changed_docs)
|
|
1347
|
+
project_doc_reference_payload = _project_doc_reference_payload(repo_root)
|
|
1348
|
+
support_indexes = _support_indexes_payload(repo_root)
|
|
1349
|
+
_write_json(pipeline_dir / "source_doc_mutation_request.json", payload)
|
|
1350
|
+
task_context.metadata.update({"payload": payload, "repo_root": repo_root, "pipeline_key": pipeline_key, "pipeline_dir": pipeline_dir, "run_id": run_id, "docs": docs, "source_docs_before": source_docs_before, "registry_before": registry_before, "assumptions": [item for item in registry_before if isinstance(item, dict)], "changed_docs": changed_docs, "inventory": inventory, "repo_inventory": repo_inventory, "requested_mode": requested_mode, "agent_runtime_mode": agent_runtime_mode, "grounded_text": grounded_text, "raw_text": str(payload.get("raw_text") or ""), "effective_mode": effective_mode, "repo_grounding_refs": repo_grounding_refs, "trust_level": trust_level, "plan_notes": plan_notes, "mutation_origin": mutation_origin, "shell_only": shell_only, "agent_runs": [], "project_doc_reference_payload": project_doc_reference_payload, "support_indexes": support_indexes})
|
|
1351
|
+
task_context.update_node(self.node_name, node_type="deterministic", run_id=run_id, effective_mode=effective_mode, agent_runtime_mode=agent_runtime_mode, pipeline_dir=str(pipeline_dir))
|
|
1352
|
+
return task_context
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
class ProductBriefSectionAgentNode(AgentNode):
|
|
1356
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1357
|
+
return AgentConfig(instructions="Mutate the product brief section from grounded evidence.", output_type=SourceDocSectionArtifact)
|
|
1358
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1359
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1360
|
+
fallback = _heuristic_section_payloads(grounded_text=task_context.metadata["grounded_text"], docs=_clone(task_context.metadata["docs"]), assumptions=list(task_context.metadata["assumptions"]), payload=task_context.metadata["payload"], requested_mode=task_context.metadata["requested_mode"], effective_mode=task_context.metadata["effective_mode"])["product_brief"]
|
|
1361
|
+
artifact, mode = _run_source_doc_agent(repo_root=repo_root, node_name=self.node_name, stage_name="product_brief", output_model=SourceDocSectionArtifact, context_payload={"grounded_text": task_context.metadata["grounded_text"], "current_doc": task_context.metadata["docs"]["product_brief.json"], "deterministic_hint": fallback.model_dump()}, guidance=load_agentic_prompt_lines("source_doc_mutation_product_brief"), fallback=fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1362
|
+
task_context.metadata["product_brief_section"] = SourceDocSectionArtifact.model_validate(artifact.model_dump() if hasattr(artifact, 'model_dump') else artifact)
|
|
1363
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode)
|
|
1364
|
+
return task_context
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
class UserWorkflowsSectionAgentNode(AgentNode):
|
|
1368
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1369
|
+
return AgentConfig(instructions="Mutate the user workflows section from grounded evidence.", output_type=SourceDocSectionArtifact)
|
|
1370
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1371
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1372
|
+
fallback = _heuristic_section_payloads(grounded_text=task_context.metadata["grounded_text"], docs=_clone(task_context.metadata["docs"]), assumptions=list(task_context.metadata["assumptions"]), payload=task_context.metadata["payload"], requested_mode=task_context.metadata["requested_mode"], effective_mode=task_context.metadata["effective_mode"])["user_workflows"]
|
|
1373
|
+
artifact, mode = _run_source_doc_agent(repo_root=repo_root, node_name=self.node_name, stage_name="user_workflows", output_model=SourceDocSectionArtifact, context_payload={"grounded_text": task_context.metadata["grounded_text"], "current_doc": task_context.metadata["docs"]["user_workflows.json"], "deterministic_hint": fallback.model_dump()}, guidance=load_agentic_prompt_lines("source_doc_mutation_user_workflows"), fallback=fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1374
|
+
task_context.metadata["user_workflows_section"] = SourceDocSectionArtifact.model_validate(artifact.model_dump() if hasattr(artifact, 'model_dump') else artifact)
|
|
1375
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode)
|
|
1376
|
+
return task_context
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
class DomainEntitiesSectionAgentNode(AgentNode):
|
|
1380
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1381
|
+
return AgentConfig(instructions="Mutate the domain entities section from grounded evidence.", output_type=SourceDocSectionArtifact)
|
|
1382
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1383
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1384
|
+
fallback = _heuristic_section_payloads(grounded_text=task_context.metadata["grounded_text"], docs=_clone(task_context.metadata["docs"]), assumptions=list(task_context.metadata["assumptions"]), payload=task_context.metadata["payload"], requested_mode=task_context.metadata["requested_mode"], effective_mode=task_context.metadata["effective_mode"])["domain_entities"]
|
|
1385
|
+
artifact, mode = _run_source_doc_agent(repo_root=repo_root, node_name=self.node_name, stage_name="domain_entities", output_model=SourceDocSectionArtifact, context_payload={"grounded_text": task_context.metadata["grounded_text"], "current_doc": task_context.metadata["docs"]["domain_entities.json"], "deterministic_hint": fallback.model_dump()}, guidance=load_agentic_prompt_lines("source_doc_mutation_domain_entities"), fallback=fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1386
|
+
task_context.metadata["domain_entities_section"] = SourceDocSectionArtifact.model_validate(artifact.model_dump() if hasattr(artifact, 'model_dump') else artifact)
|
|
1387
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode)
|
|
1388
|
+
return task_context
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
class SourceSectionConcurrentNode(ConcurrentNode):
|
|
1392
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1393
|
+
await self.execute_nodes_concurrently(task_context)
|
|
1394
|
+
task_context.update_node(self.node_name, node_type="concurrent", children=["ProductBriefSectionAgentNode", "UserWorkflowsSectionAgentNode", "DomainEntitiesSectionAgentNode"])
|
|
1395
|
+
return task_context
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
class SourceDocCoherenceNode(AgentNode):
|
|
1399
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1400
|
+
return AgentConfig(instructions="Reconcile section outputs into a coherent source-doc set.", output_type=SourceDocCoherenceArtifact)
|
|
1401
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1402
|
+
docs = task_context.metadata["docs"]
|
|
1403
|
+
changed_docs = task_context.metadata["changed_docs"]
|
|
1404
|
+
assumptions = list(task_context.metadata["assumptions"])
|
|
1405
|
+
payload = task_context.metadata["payload"]
|
|
1406
|
+
for key, doc_name in [("product_brief_section", "product_brief.json"), ("user_workflows_section", "user_workflows.json"), ("domain_entities_section", "domain_entities.json")]:
|
|
1407
|
+
section = task_context.metadata[key]
|
|
1408
|
+
if section.section_payload != docs[doc_name]:
|
|
1409
|
+
docs[doc_name] = section.section_payload
|
|
1410
|
+
_mark_changed(changed_docs, doc_name)
|
|
1411
|
+
assumptions.extend([item for item in section.assumption_entries if isinstance(item, dict)])
|
|
1412
|
+
if task_context.metadata["requested_mode"] == "ideation_mutation":
|
|
1413
|
+
product = docs["product_brief.json"]
|
|
1414
|
+
if not str(product.get("problem_statement") or "").strip():
|
|
1415
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="problem_statement", topic_key="primary_problem", current_value="User needs a better software-supported workflow, but the exact pain point is still unconfirmed.", reasoning="The intake requests software work but does not yet name the concrete pain clearly.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1416
|
+
if not list((product.get("scope") or {}).get("in_scope") or []):
|
|
1417
|
+
_record_assumption(assumptions=assumptions, doc_path="ai_docs/context/source_docs/product_brief.json", section_key="scope", topic_key="initial_scope", current_value=["first build should focus on the smallest workflow that resolves the request"], reasoning="The message asks for software work but does not yet define a narrow first release scope.", source="grounded_inference", message_id=str(payload["message_id"]))
|
|
1418
|
+
docs["assumptions_registry.json"] = {"assumptions": assumptions}
|
|
1419
|
+
coherence_fallback = SourceDocCoherenceArtifact(docs={name: _clone(docs[name]) for name in CANONICAL_SOURCE_DOCS}, assumptions=[item for item in assumptions if isinstance(item, dict)], notes=["Deterministic source-doc coherence reconciliation."], mode="heuristic")
|
|
1420
|
+
coherence_artifact, mode = _run_source_doc_agent(repo_root=task_context.metadata["repo_root"], node_name=self.node_name, stage_name="source_doc_coherence", output_model=SourceDocCoherenceArtifact, context_payload={"grounded_text": task_context.metadata["grounded_text"], "docs": {name: docs[name] for name in CANONICAL_SOURCE_DOCS}, "assumptions": assumptions, "changed_docs": changed_docs}, guidance=load_agentic_prompt_lines("source_doc_mutation_source_doc_coherence"), fallback=coherence_fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1421
|
+
docs.update({name: value for name, value in coherence_artifact.docs.items() if name in CANONICAL_SOURCE_DOCS and isinstance(value, dict)})
|
|
1422
|
+
docs["assumptions_registry.json"] = {"assumptions": [item for item in coherence_artifact.assumptions if isinstance(item, dict)]}
|
|
1423
|
+
reconciled_registry, registry_changed = reconcile_assumptions_registry(registry_payload=docs["assumptions_registry.json"], source_docs_by_name=docs, changed_docs=changed_docs)
|
|
1424
|
+
docs["assumptions_registry.json"] = reconciled_registry
|
|
1425
|
+
task_context.metadata["assumptions"] = [item for item in list(reconciled_registry.get("assumptions") or []) if isinstance(item, dict)]
|
|
1426
|
+
if registry_changed:
|
|
1427
|
+
_mark_changed(changed_docs, "assumptions_registry.json")
|
|
1428
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode, changed_docs=list(changed_docs), assumptions_total=len(task_context.metadata["assumptions"]))
|
|
1429
|
+
return task_context
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class SourceDocEnrichmentCoherenceNode(AgentNode):
|
|
1433
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1434
|
+
return AgentConfig(instructions="Perform a final sparse-safe source-doc enrichment and coherence pass before project-doc sync.", output_type=SourceDocCoherenceArtifact)
|
|
1435
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1436
|
+
docs = task_context.metadata["docs"]
|
|
1437
|
+
assumptions = list(task_context.metadata["assumptions"])
|
|
1438
|
+
changed_docs = task_context.metadata["changed_docs"]
|
|
1439
|
+
enriched_docs, enriched_assumptions, enrichment_notes = _enrich_sparse_source_docs(grounded_text=task_context.metadata["grounded_text"], docs={name: _clone(docs[name]) for name in CANONICAL_SOURCE_DOCS}, assumptions=assumptions, payload=task_context.metadata["payload"], requested_mode=task_context.metadata["requested_mode"])
|
|
1440
|
+
fallback = SourceDocCoherenceArtifact(docs=enriched_docs, assumptions=[item for item in enriched_assumptions if isinstance(item, dict)], notes=enrichment_notes + ["Deterministic pre-sync source-doc enrichment/coherence pass."], mode="heuristic")
|
|
1441
|
+
artifact, mode = _run_source_doc_agent(repo_root=task_context.metadata["repo_root"], node_name=self.node_name, stage_name="source_doc_enrichment_coherence", output_model=SourceDocCoherenceArtifact, context_payload={"grounded_text": task_context.metadata["grounded_text"], "docs": {name: docs[name] for name in CANONICAL_SOURCE_DOCS}, "assumptions": assumptions, "support_indexes": task_context.metadata.get("support_indexes") or {}, "changed_docs": changed_docs}, guidance=load_agentic_prompt_lines("source_doc_mutation_source_doc_enrichment_coherence"), fallback=fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1442
|
+
notes = list(getattr(artifact, "notes", []) or [])
|
|
1443
|
+
before_docs = {name: json.dumps(docs.get(name, {}), sort_keys=True) for name in CANONICAL_SOURCE_DOCS}
|
|
1444
|
+
docs.update({name: value for name, value in artifact.docs.items() if name in CANONICAL_SOURCE_DOCS and isinstance(value, dict)})
|
|
1445
|
+
docs["assumptions_registry.json"] = {"assumptions": [item for item in artifact.assumptions if isinstance(item, dict)]}
|
|
1446
|
+
task_context.metadata["assumptions"] = [item for item in list(docs["assumptions_registry.json"].get("assumptions") or []) if isinstance(item, dict)]
|
|
1447
|
+
for name in CANONICAL_SOURCE_DOCS:
|
|
1448
|
+
if before_docs[name] != json.dumps(docs.get(name, {}), sort_keys=True):
|
|
1449
|
+
_mark_changed(changed_docs, name)
|
|
1450
|
+
task_context.metadata["source_doc_enrichment_notes"] = notes
|
|
1451
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode, notes=notes, changed_docs=list(changed_docs))
|
|
1452
|
+
return task_context
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
class PersistSourceDocsAndAssumptionsNode(Node):
|
|
1456
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1457
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1458
|
+
docs = task_context.metadata["docs"]
|
|
1459
|
+
changed_docs = task_context.metadata["changed_docs"]
|
|
1460
|
+
source_docs_dir = _source_docs_dir(repo_root)
|
|
1461
|
+
source_docs_dir.mkdir(parents=True, exist_ok=True)
|
|
1462
|
+
docs_to_write = list(CANONICAL_SOURCE_DOCS) if task_context.metadata["shell_only"] else list(dict.fromkeys(changed_docs + ["assumptions_registry.json"]))
|
|
1463
|
+
for name in docs_to_write:
|
|
1464
|
+
_write_json(source_docs_dir / name, docs[name])
|
|
1465
|
+
_mark_changed(changed_docs, "assumptions_registry.json")
|
|
1466
|
+
assumptions_delta = _compute_assumptions_delta(task_context.metadata["registry_before"], task_context.metadata["assumptions"])
|
|
1467
|
+
changed_sections = _compute_changed_sections(before_docs=task_context.metadata["source_docs_before"], after_docs=docs, changed_docs=changed_docs)
|
|
1468
|
+
task_context.metadata.update({"source_docs_dir": source_docs_dir, "assumptions_delta": assumptions_delta, "changed_sections": changed_sections})
|
|
1469
|
+
task_context.update_node(self.node_name, node_type="deterministic", docs_to_write=docs_to_write)
|
|
1470
|
+
return task_context
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
class ProjectDocImpactRoutingNode(Node):
|
|
1474
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1475
|
+
changed_docs = list(task_context.metadata["changed_docs"])
|
|
1476
|
+
changed_sections = list(task_context.metadata.get("changed_sections") or [])
|
|
1477
|
+
support_indexes = task_context.metadata.get("support_indexes") or {}
|
|
1478
|
+
routing_targets = _project_doc_target_payloads(repo_root=task_context.metadata["repo_root"], docs=task_context.metadata["docs"], changed_docs=changed_docs, changed_sections=changed_sections, support_indexes=support_indexes)
|
|
1479
|
+
impacted = [item["project_doc_name"] for item in routing_targets if item["directly_impacted_by_changed_docs"]]
|
|
1480
|
+
routing_payload = {
|
|
1481
|
+
"classification": "support_docs",
|
|
1482
|
+
"reference_map_doc": PROJECT_DOC_REFERENCE_DOCS["reference_map"],
|
|
1483
|
+
"support_indexes": {name: {"path": value.get("path"), "classification": value.get("classification"), "loaded": value.get("loaded"), "contract": value.get("contract")} for name, value in (support_indexes.get("indexes") or {}).items()},
|
|
1484
|
+
"changed_docs": changed_docs,
|
|
1485
|
+
"changed_sections": changed_sections,
|
|
1486
|
+
"project_doc_targets": [
|
|
1487
|
+
{
|
|
1488
|
+
"project_doc_name": item["project_doc_name"],
|
|
1489
|
+
"primary_source_docs": item["primary_source_docs"],
|
|
1490
|
+
"supporting_source_docs": item["supporting_source_docs"],
|
|
1491
|
+
"directly_impacted_by_changed_docs": item["directly_impacted_by_changed_docs"],
|
|
1492
|
+
"routed_changed_sections": item["routed_changed_sections"],
|
|
1493
|
+
}
|
|
1494
|
+
for item in routing_targets
|
|
1495
|
+
],
|
|
1496
|
+
}
|
|
1497
|
+
task_context.metadata["project_doc_target_payloads"] = routing_targets
|
|
1498
|
+
task_context.metadata["project_doc_impact_routing"] = routing_payload
|
|
1499
|
+
task_context.metadata["impacted_project_docs"] = impacted
|
|
1500
|
+
task_context.update_node(self.node_name, node_type="deterministic", impacted_project_docs=impacted, routing_targets=[item["project_doc_name"] for item in routing_targets])
|
|
1501
|
+
return task_context
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
class SpawnProjectDocMutationSubagentsNode(Node):
|
|
1505
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1506
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1507
|
+
pipeline_dir = task_context.metadata["pipeline_dir"]
|
|
1508
|
+
shell_only = task_context.metadata["shell_only"]
|
|
1509
|
+
reference_payload = task_context.metadata.get("project_doc_reference_payload") or {}
|
|
1510
|
+
subagents: list[dict[str, Any]] = []
|
|
1511
|
+
for item in task_context.metadata.get("project_doc_target_payloads") or []:
|
|
1512
|
+
md_name = item["project_doc_name"]
|
|
1513
|
+
output_path = pipeline_dir / f"subagent_{md_name.replace('.', '_')}.json"
|
|
1514
|
+
target_payload = dict(item)
|
|
1515
|
+
target_payload["reference_docs"] = reference_payload
|
|
1516
|
+
target_payload["support_indexes"] = task_context.metadata.get("support_indexes") or {}
|
|
1517
|
+
cmd = [sys.executable, "-c", f"from devflow_engine.source_doc_mutation_dag import _run_project_doc_subagent_cli; _run_project_doc_subagent_cli(repo_root={str(repo_root)!r}, output_path={str(output_path)!r}, project_doc_name={md_name!r}, target_payload={target_payload!r}, shell_only={shell_only!r}, runtime_mode={task_context.metadata['agent_runtime_mode']!r})"]
|
|
1518
|
+
proc = subprocess.Popen(cmd, cwd=str(repo_root))
|
|
1519
|
+
subagents.append({"project_doc_name": md_name, "pid": proc.pid, "output_path": str(output_path), "primary_source_docs": item.get("primary_source_docs") or [], "supporting_source_docs": item.get("supporting_source_docs") or [], "directly_impacted_by_changed_docs": item.get("directly_impacted_by_changed_docs") or [], "reference_docs_supplied": [value.get("path") for value in reference_payload.values() if isinstance(value, dict) and value.get("path")]})
|
|
1520
|
+
task_context.metadata["project_doc_subagents"] = subagents
|
|
1521
|
+
task_context.update_node(self.node_name, node_type="deterministic", spawned_subagents=subagents)
|
|
1522
|
+
return task_context
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
class AwaitProjectDocMutationSubagentsNode(Node):
|
|
1526
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1527
|
+
results: list[dict[str, Any]] = []
|
|
1528
|
+
for item in task_context.metadata.get("project_doc_subagents") or []:
|
|
1529
|
+
pid = int(item["pid"])
|
|
1530
|
+
try:
|
|
1531
|
+
os.waitpid(pid, 0)
|
|
1532
|
+
except ChildProcessError:
|
|
1533
|
+
pass
|
|
1534
|
+
payload = _load_json_dict(Path(item["output_path"]), default={"project_doc_name": item["project_doc_name"], "status": "missing"})
|
|
1535
|
+
results.append(payload)
|
|
1536
|
+
task_context.metadata["project_doc_subagent_results"] = results
|
|
1537
|
+
task_context.update_node(self.node_name, node_type="deterministic", awaited=len(results))
|
|
1538
|
+
return task_context
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
class ProjectDocCoherenceNode(AgentNode):
|
|
1542
|
+
def get_agent_config(self) -> AgentConfig:
|
|
1543
|
+
return AgentConfig(instructions="Review the full project-doc family for coherence against canonical source docs and project-doc support contracts.", output_type=ProjectDocCoherenceArtifact)
|
|
1544
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1545
|
+
results = task_context.metadata.get("project_doc_subagent_results") or []
|
|
1546
|
+
notes = [f"{item.get('project_doc_name')}: {item.get('mode')}" for item in results]
|
|
1547
|
+
fallback = ProjectDocCoherenceArtifact(notes=notes + ["Fallback coherence output is thin; richer coherence findings require the real agent runtime."], mode="heuristic")
|
|
1548
|
+
artifact, mode = _run_source_doc_agent(repo_root=task_context.metadata["repo_root"], node_name=self.node_name, stage_name="project_doc_coherence", output_model=ProjectDocCoherenceArtifact, context_payload={"canonical_source_docs": {name: task_context.metadata["docs"].get(name, {}) for name in CANONICAL_SOURCE_DOCS}, "project_doc_results": results, "project_doc_impact_routing": task_context.metadata.get("project_doc_impact_routing") or {}, "project_doc_reference_docs": task_context.metadata.get("project_doc_reference_payload") or {}}, guidance=load_agentic_prompt_lines("source_doc_mutation_project_doc_coherence"), fallback=fallback, runtime_mode=task_context.metadata["agent_runtime_mode"], task_context=task_context)
|
|
1549
|
+
task_context.metadata["project_doc_coherence"] = artifact.model_dump() if hasattr(artifact, "model_dump") else artifact
|
|
1550
|
+
task_context.update_node(self.node_name, node_type="agentic", response_mode=mode, notes=artifact.notes)
|
|
1551
|
+
return task_context
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
class PersistProjectDocsNode(Node):
|
|
1555
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
1556
|
+
payload = task_context.metadata["payload"]
|
|
1557
|
+
repo_root = task_context.metadata["repo_root"]
|
|
1558
|
+
pipeline_dir = task_context.metadata["pipeline_dir"]
|
|
1559
|
+
run_id = task_context.metadata["run_id"]
|
|
1560
|
+
source_docs_dir = task_context.metadata["source_docs_dir"]
|
|
1561
|
+
project_docs_root = _v2_project_docs_dir(repo_root) / "source_docs"
|
|
1562
|
+
project_docs_root.mkdir(parents=True, exist_ok=True)
|
|
1563
|
+
updated_outputs: list[str] = []
|
|
1564
|
+
for item in task_context.metadata.get("project_doc_subagent_results") or []:
|
|
1565
|
+
md_name = str(item.get("project_doc_name") or "")
|
|
1566
|
+
if not md_name:
|
|
1567
|
+
continue
|
|
1568
|
+
path = project_docs_root / md_name
|
|
1569
|
+
path.write_text(str(item.get("content") or _render_markdown(md_name.replace('.md', '.json'), task_context.metadata["docs"].get(md_name.replace('.md', '.json'), {}), shell_only=task_context.metadata["shell_only"])), encoding="utf-8")
|
|
1570
|
+
updated_outputs.append(str(path.relative_to(repo_root)))
|
|
1571
|
+
index_path = project_docs_root / "index.md"
|
|
1572
|
+
index_intro = "Explicit project-doc subagent outputs derived from canonical source docs."
|
|
1573
|
+
if task_context.metadata["shell_only"]:
|
|
1574
|
+
index_intro = "This directory contains scaffold-only derived project doc shells. Repo evidence was too sparse to infer a grounded project shape."
|
|
1575
|
+
index_lines = ["# Project Doc Family Index", "", index_intro, ""]
|
|
1576
|
+
for md_name in PROJECT_DOC_TARGETS:
|
|
1577
|
+
index_lines.append(f"- `source_docs/{md_name}`")
|
|
1578
|
+
index_path.write_text("\n".join(index_lines) + "\n", encoding="utf-8")
|
|
1579
|
+
updated_outputs.append(str(index_path.relative_to(repo_root)))
|
|
1580
|
+
project_doc_sync_result = {"status": "completed", "derived_project_docs_root": str(_v2_project_docs_dir(repo_root)), "derived_source_doc_root": str(project_docs_root), "updated_outputs": updated_outputs, "changed_docs": task_context.metadata["changed_docs"], "impacted_project_docs": task_context.metadata.get("impacted_project_docs") or [], "sync_mode": "subagent_project_doc_render", "shell_only": task_context.metadata["shell_only"], "agent_runtime_mode": task_context.metadata["agent_runtime_mode"], "project_doc_coherence": task_context.metadata.get("project_doc_coherence") or {}, "source_doc_enrichment_notes": task_context.metadata.get("source_doc_enrichment_notes") or [], "project_doc_impact_routing": task_context.metadata.get("project_doc_impact_routing") or {}, "reference_docs_supplied": {key: value.get("path") for key, value in (task_context.metadata.get("project_doc_reference_payload") or {}).items() if isinstance(value, dict)}, "support_indexes": {name: {"path": value.get("path"), "classification": value.get("classification"), "loaded": value.get("loaded"), "contract": value.get("contract")} for name, value in ((task_context.metadata.get("support_indexes") or {}).get("indexes") or {}).items()}, "subagent_results": task_context.metadata.get("project_doc_subagent_results") or []}
|
|
1581
|
+
assumptions = task_context.metadata["assumptions"]
|
|
1582
|
+
changed_docs = task_context.metadata["changed_docs"]
|
|
1583
|
+
mutation_plan = {"requested_mode": task_context.metadata["requested_mode"], "effective_mode": task_context.metadata["effective_mode"], "agent_runtime_mode": task_context.metadata["agent_runtime_mode"], "mutation_origin": task_context.metadata["mutation_origin"], "trust_level": task_context.metadata["trust_level"], "repo_grounding_refs": task_context.metadata["repo_grounding_refs"], "plan_notes": task_context.metadata["plan_notes"], "change_intent": {name: ("no_op_shell" if task_context.metadata["shell_only"] else "mutate" if name in changed_docs else "no_op") for name in CANONICAL_SOURCE_DOCS}, "decision_rule": task_context.metadata["repo_inventory"].get("decision_rule"), "project_doc_reference_docs": {key: value.get("path") for key, value in (task_context.metadata.get("project_doc_reference_payload") or {}).items() if isinstance(value, dict)}, "support_indexes": {name: value.get("path") for name, value in ((task_context.metadata.get("support_indexes") or {}).get("indexes") or {}).items()}, "project_doc_target_family": list(PROJECT_DOC_TARGETS), "project_doc_impact_routing": task_context.metadata.get("project_doc_impact_routing") or {}, "node_contracts": {"NormalizeAndGatherSupportContextNode": "deterministic request normalization, inventory, and support context assembly", "ProductBriefSectionAgentNode": "agentic source section mutation", "UserWorkflowsSectionAgentNode": "agentic source section mutation", "DomainEntitiesSectionAgentNode": "agentic source section mutation", "SourceDocCoherenceNode": "agentic coherence pass over section outputs", "SourceDocEnrichmentCoherenceNode": "agentic final sparse-safe source-doc enrichment/coherence pass before project-doc sync", "PersistSourceDocsAndAssumptionsNode": "deterministic canonical source-doc persistence", "ProjectDocImpactRoutingNode": "deterministic project-doc routing using source-to-project-doc reference contracts", "SpawnProjectDocMutationSubagentsNode": "deterministic orchestration that spawns project-doc mutation subagents with explicit reference-doc grounding", "AwaitProjectDocMutationSubagentsNode": "deterministic join waiting for all project-doc mutation subagents", "ProjectDocCoherenceNode": "agentic coherence pass over project-doc outputs using project-doc support contracts", "PersistProjectDocsNode": "deterministic artifact packaging and project-doc persistence"}}
|
|
1584
|
+
source_doc_change_set = {"canonical_source_docs_root": str(source_docs_dir), "changed_docs": changed_docs, "before": {name: task_context.metadata["source_docs_before"].get(name, {}) for name in changed_docs}, "after": {name: task_context.metadata["docs"].get(name, {}) for name in changed_docs}, "requested_mode": task_context.metadata["requested_mode"], "effective_mode": task_context.metadata["effective_mode"], "agent_runtime_mode": task_context.metadata["agent_runtime_mode"], "mutation_mode": "agentic_scaffold_only" if task_context.metadata["shell_only"] else "agentic_source_doc_mutation", "source_doc_enrichment_notes": task_context.metadata.get("source_doc_enrichment_notes") or [], "repo_grounding_refs": task_context.metadata["repo_grounding_refs"], "changed_sections": task_context.metadata.get("changed_sections") or []}
|
|
1585
|
+
result_payload = {"contract": DAG_ID, "run_id": run_id, "status": "completed", "artifact_root": str(pipeline_dir), "canonical_source_docs_root": str(source_docs_dir), "derived_project_docs_root": str(_v2_project_docs_dir(repo_root)), "result_artifact": "source_doc_mutation_result.json", "inventory_artifact": "source_doc_inventory.json", "plan_artifact": "source_doc_mutation_plan.json", "change_set_artifact": "source_doc_change_set.json", "assumptions_delta_artifact": "assumptions_delta.json", "project_doc_sync_artifact": "project_doc_sync_result.json", "agent_runs_artifact": "agent_runs.json", "changed_docs": changed_docs, "requested_mode": task_context.metadata["requested_mode"], "effective_mode": task_context.metadata["effective_mode"], "agent_runtime_mode": task_context.metadata["agent_runtime_mode"], "trust_level": task_context.metadata["trust_level"], "project_doc_target_family": list(PROJECT_DOC_TARGETS), "project_doc_reference_docs": {key: value.get("path") for key, value in (task_context.metadata.get("project_doc_reference_payload") or {}).items() if isinstance(value, dict)}, "support_indexes": {name: {"path": value.get("path"), "classification": value.get("classification"), "loaded": value.get("loaded"), "contract": value.get("contract")} for name, value in ((task_context.metadata.get("support_indexes") or {}).get("indexes") or {}).items()}, "assumption_status": {"open_count": len([item for item in assumptions if str(item.get("status") or "open") == "open"]), "total_count": len(assumptions)}, "dag_shape": {"workflow_schema": DAG_ID, "start_node": "NormalizeAndGatherSupportContextNode", "node_sequence": ["NormalizeAndGatherSupportContextNode", "SourceSectionConcurrentNode", "ProductBriefSectionAgentNode", "UserWorkflowsSectionAgentNode", "DomainEntitiesSectionAgentNode", "SourceDocCoherenceNode", "SourceDocEnrichmentCoherenceNode", "PersistSourceDocsAndAssumptionsNode", "ProjectDocImpactRoutingNode", "SpawnProjectDocMutationSubagentsNode", "AwaitProjectDocMutationSubagentsNode", "ProjectDocCoherenceNode", "PersistProjectDocsNode"], "deterministic_nodes": ["NormalizeAndGatherSupportContextNode", "PersistSourceDocsAndAssumptionsNode", "ProjectDocImpactRoutingNode", "SpawnProjectDocMutationSubagentsNode", "AwaitProjectDocMutationSubagentsNode", "PersistProjectDocsNode"], "agent_nodes": ["ProductBriefSectionAgentNode", "UserWorkflowsSectionAgentNode", "DomainEntitiesSectionAgentNode", "SourceDocCoherenceNode", "SourceDocEnrichmentCoherenceNode", "ProjectDocCoherenceNode"], "concurrent_section_nodes": ["ProductBriefSectionAgentNode", "UserWorkflowsSectionAgentNode", "DomainEntitiesSectionAgentNode"], "parallel_source_section_nodes": ["ProductBriefSectionAgentNode", "UserWorkflowsSectionAgentNode", "DomainEntitiesSectionAgentNode"], "project_doc_subagent_barrier": {"spawn_node": "SpawnProjectDocMutationSubagentsNode", "await_node": "AwaitProjectDocMutationSubagentsNode", "all_spawned_subagents_must_complete_before": "ProjectDocCoherenceNode"}, "future_agent_nodes": ["SourceDocSemanticMutationAgentNode"]}, "derivation": {"mutation_mode": "deterministic_scaffold_only" if task_context.metadata["shell_only"] else "deterministic_bootstrap_patch", "actual_runtime_mode": "agentic_scaffold_only" if task_context.metadata["shell_only"] else "agentic_source_doc_mutation", "writes_canonical_source_docs": True, "writes_derived_project_docs": True, "pretend_agentic": False, "repo_grounded": task_context.metadata["mutation_origin"] == "repo_grounded_bootstrap", "shell_only": task_context.metadata["shell_only"], "used_real_llm_runtime": task_context.metadata["agent_runtime_mode"] == "real" and not task_context.metadata["shell_only"]}, "repo_bootstrap_decision": {"requested_mode": task_context.metadata["requested_mode"], "effective_mode": task_context.metadata["effective_mode"], "repo_grounding_refs": task_context.metadata["repo_grounding_refs"], "signal_summary": task_context.metadata["repo_inventory"]}, "agent_runtime_summary": {"requested_mode": task_context.metadata["agent_runtime_mode"], "agent_run_count": len(task_context.metadata.get("agent_runs") or []), "response_modes": [item.get("response_mode") for item in task_context.metadata.get("agent_runs") or []]}, "gaps": ["SourceDocSemanticMutationAgentNode no longer owns the happy path; it has been replaced by explicit ProductBriefSectionAgentNode/UserWorkflowsSectionAgentNode/DomainEntitiesSectionAgentNode plus SourceDocCoherenceNode.", "Contradiction-specific routing/gates/artifacts are still not implemented in this DAG.", "planning summary/open questions/scope readiness artifacts are still not emitted by this DAG.", "Project-doc richness still depends on upstream source-doc breadth and the real LLM runtime; stub runs remain intentionally thin."]}
|
|
1586
|
+
_write_json(pipeline_dir / "source_doc_inventory.json", task_context.metadata["inventory"])
|
|
1587
|
+
_write_json(pipeline_dir / "source_doc_mutation_plan.json", mutation_plan)
|
|
1588
|
+
_write_json(pipeline_dir / "source_doc_change_set.json", source_doc_change_set)
|
|
1589
|
+
_write_json(pipeline_dir / "assumptions_delta.json", task_context.metadata["assumptions_delta"])
|
|
1590
|
+
_write_json(pipeline_dir / "project_doc_sync_result.json", project_doc_sync_result)
|
|
1591
|
+
_write_json(pipeline_dir / "agent_runs.json", {"agent_runtime_mode": task_context.metadata["agent_runtime_mode"], "runs": task_context.metadata.get("agent_runs") or []})
|
|
1592
|
+
_write_json(pipeline_dir / "source_doc_mutation_result.json", result_payload)
|
|
1593
|
+
mutation_ref = {"contract": "source_doc_mutation_dag.ref.v1", "dag_id": DAG_ID, "run_id": run_id, "status": result_payload["status"], "artifact_root": str(pipeline_dir), "result_artifact": result_payload["result_artifact"], "change_set_artifact": result_payload["change_set_artifact"], "assumptions_delta_artifact": result_payload["assumptions_delta_artifact"], "project_doc_sync_artifact": result_payload["project_doc_sync_artifact"], "agent_runs_artifact": result_payload["agent_runs_artifact"], "changed_docs": changed_docs, "trust_level": task_context.metadata["trust_level"], "agent_runtime_mode": task_context.metadata["agent_runtime_mode"]}
|
|
1594
|
+
task_context.metadata["final_result"] = SourceDocMutationDagResult(run_id=run_id, pipeline_dir=pipeline_dir, mutation_ref=mutation_ref, result=result_payload)
|
|
1595
|
+
task_context.update_node(self.node_name, node_type="deterministic", status="completed")
|
|
1596
|
+
return task_context
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
class SourceDocMutationDagWorkflow(Workflow):
|
|
1600
|
+
workflow_schema = WorkflowSchema(description="Current source_doc_mutation_dag aligned to explicit source-section concurrency and project-doc subagent orchestration.", event_schema=SourceDocMutationDagEvent, start=NormalizeAndGatherSupportContextNode, nodes=[NodeConfig(node=NormalizeAndGatherSupportContextNode, connections=[SourceSectionConcurrentNode]), NodeConfig(node=SourceSectionConcurrentNode, connections=[SourceDocCoherenceNode], concurrent_nodes=[ProductBriefSectionAgentNode, UserWorkflowsSectionAgentNode, DomainEntitiesSectionAgentNode]), NodeConfig(node=SourceDocCoherenceNode, connections=[SourceDocEnrichmentCoherenceNode]), NodeConfig(node=SourceDocEnrichmentCoherenceNode, connections=[PersistSourceDocsAndAssumptionsNode]), NodeConfig(node=PersistSourceDocsAndAssumptionsNode, connections=[ProjectDocImpactRoutingNode]), NodeConfig(node=ProjectDocImpactRoutingNode, connections=[SpawnProjectDocMutationSubagentsNode]), NodeConfig(node=SpawnProjectDocMutationSubagentsNode, connections=[AwaitProjectDocMutationSubagentsNode]), NodeConfig(node=AwaitProjectDocMutationSubagentsNode, connections=[ProjectDocCoherenceNode]), NodeConfig(node=ProjectDocCoherenceNode, connections=[PersistProjectDocsNode]), NodeConfig(node=PersistProjectDocsNode, connections=[]), ])
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def run_source_doc_mutation_dag(*, request: dict[str, Any]) -> SourceDocMutationDagResult:
|
|
1604
|
+
workflow = SourceDocMutationDagWorkflow()
|
|
1605
|
+
task_context = workflow.run(request)
|
|
1606
|
+
return task_context.metadata["final_result"]
|