codd-dev 1.26.0__tar.gz → 1.27.0__tar.gz
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.
- {codd_dev-1.26.0 → codd_dev-1.27.0}/PKG-INFO +1 -1
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/cli.py +402 -5
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/__init__.py +8 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/builder.py +14 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/means_catalog.py +1 -1
- codd_dev-1.27.0/codd/repair/__init__.py +15 -0
- codd_dev-1.27.0/codd/repair/approval_repair.py +127 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/history.py +3 -2
- codd_dev-1.27.0/codd/repair/loop.py +298 -0
- codd_dev-1.27.0/codd/repair/verify_runner.py +355 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/pyproject.toml +1 -1
- codd_dev-1.26.0/codd/llm/templates/verification_means_catalog.yaml +0 -6
- codd_dev-1.26.0/codd/repair/__init__.py +0 -5
- {codd_dev-1.26.0 → codd_dev-1.27.0}/.gitignore +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/LICENSE +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/README.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/__main__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/_git_helper.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/ask_user_question_adapter.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/assembler.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/bridge.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/clustering.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coherence_adapters.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coherence_engine.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/config.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/contracts.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coverage_auditor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coverage_metrics.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/depends_on_consistency.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/deployment_completeness.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/edge_validity.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/node_completeness.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/task_completion.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/transitive_closure.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/user_journey_coherence.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/cli.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/cpp_embedded.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/csharp.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/elixir.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/generic.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/iot.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/java.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/kotlin.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/mobile.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/ruby.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/rust.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/scala.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/swift.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/web.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/runner.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/defaults.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/app_service.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/base.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/docker_compose.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployer.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/checks/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/runtime_capability_inference.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/ai_command.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/llm_consideration.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/schema/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/schema/prisma.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/target/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/target/docker_compose.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/assertion_handlers.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_browser.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_engines.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_launchers.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_wire.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/curl.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/form_strategies.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/playwright.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/design_md.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/drift.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_generator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_runner.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/env_refs.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/extract_ai.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixer.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/generator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/graph.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hitl_session.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/pre-commit +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/codex_hook.sh +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/implementer.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/inheritance.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/knowledge_fetcher.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/lexicon.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/approval.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/means_catalog_loader.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/parser.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/prompt_builder.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/strategy_validator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/templates/meta_instruction.md +0 -0
- {codd_dev-1.26.0/codd/deployment/defaults → codd_dev-1.27.0/codd/llm/templates}/verification_means_catalog.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/mcp_server.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/measure.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/parsing.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/planner.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/policy.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/cli.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/iot.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/mobile.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/web.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/propagate.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/propagator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/registry.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/engine.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/git_patcher.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/llm_repair_engine.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/schema.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/templates/analyze_meta.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/templates/propose_meta.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair_slice.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require_plugins.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require_propagate.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/web.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts_deriver.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness_auditor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/restore.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/routes_extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/scanner.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/schema_refs.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_flow_validator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_transition_extractor.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_transitions/defaults.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/synth.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extract_ai_prompt_baseline.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/lexicon_questions.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/lexicon_schema.yaml +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/traceability.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/validator.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/__init__.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/events.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/propagation_log.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/propagation_pipeline.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/test_runner.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/watcher.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/wiring.py +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/docs/cookbook/cdp_browser/README.md +0 -0
- {codd_dev-1.26.0 → codd_dev-1.27.0}/docs/requirements/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codd-dev
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.0
|
|
4
4
|
Summary: CoDD: Coherence-Driven Development — cross-artifact change impact analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/yohey-w/codd-dev
|
|
6
6
|
Project-URL: Repository, https://github.com/yohey-w/codd-dev
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CoDD CLI — codd init / scan / impact / require / plan."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import asdict, is_dataclass
|
|
3
|
+
from dataclasses import asdict, dataclass, is_dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import shutil
|
|
@@ -21,8 +22,15 @@ class CoddCLIError(RuntimeError):
|
|
|
21
22
|
"""Error raised for CLI-facing validation failures."""
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
@dataclass
|
|
26
|
+
class _CliVerificationResult:
|
|
27
|
+
passed: bool
|
|
28
|
+
exit_code: int
|
|
29
|
+
failure: Any | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run_pro_command(name: str, **kwargs):
|
|
33
|
+
"""Dispatch a Pro-only command when the bridge plugin is installed."""
|
|
26
34
|
handler = get_command_handler(name)
|
|
27
35
|
if handler is None:
|
|
28
36
|
click.echo(PRO_COMMAND_INSTALL_MESSAGE)
|
|
@@ -1046,7 +1054,20 @@ def assemble(path: str, output_dir: str | None, ai_cmd: str | None):
|
|
|
1046
1054
|
default=False,
|
|
1047
1055
|
help="Run npx @google/design.md lint on DESIGN.md (skip if npx unavailable).",
|
|
1048
1056
|
)
|
|
1049
|
-
|
|
1057
|
+
@click.option("--auto-repair", is_flag=True, default=False, help="Run RepairLoop when verification fails")
|
|
1058
|
+
@click.option("--max-attempts", default=None, type=click.IntRange(min=1), help="Maximum repair attempts")
|
|
1059
|
+
@click.option("--engine", "engine_name", default=None, help="Repair engine name")
|
|
1060
|
+
def verify(
|
|
1061
|
+
path: str,
|
|
1062
|
+
sprint: int | None,
|
|
1063
|
+
e2e: bool,
|
|
1064
|
+
deploy: bool,
|
|
1065
|
+
base_url: str | None,
|
|
1066
|
+
design_md: bool,
|
|
1067
|
+
auto_repair: bool,
|
|
1068
|
+
max_attempts: int | None,
|
|
1069
|
+
engine_name: str | None,
|
|
1070
|
+
) -> None:
|
|
1050
1071
|
"""Run build + test verification and trace failures to design documents."""
|
|
1051
1072
|
if design_md:
|
|
1052
1073
|
_run_design_md_lint(Path(path).resolve())
|
|
@@ -1055,7 +1076,31 @@ def verify(path: str, sprint: int | None, e2e: bool, deploy: bool, base_url: str
|
|
|
1055
1076
|
from codd.e2e_runner import run_e2e
|
|
1056
1077
|
run_e2e(path=path, deploy=deploy, base_url=base_url)
|
|
1057
1078
|
return
|
|
1058
|
-
|
|
1079
|
+
|
|
1080
|
+
if not auto_repair:
|
|
1081
|
+
_run_pro_command("verify", path=path, sprint=sprint)
|
|
1082
|
+
return
|
|
1083
|
+
|
|
1084
|
+
project_root = Path(path).resolve()
|
|
1085
|
+
result = _run_verify_once(path=path, sprint=sprint)
|
|
1086
|
+
if result.passed:
|
|
1087
|
+
return
|
|
1088
|
+
|
|
1089
|
+
repair_config = _load_required_repair_config(project_root)
|
|
1090
|
+
if repair_config is None:
|
|
1091
|
+
raise SystemExit(1)
|
|
1092
|
+
|
|
1093
|
+
outcome = _run_repair_loop(
|
|
1094
|
+
project_root,
|
|
1095
|
+
result.failure,
|
|
1096
|
+
repair_config=repair_config,
|
|
1097
|
+
max_attempts=max_attempts,
|
|
1098
|
+
engine_name=engine_name,
|
|
1099
|
+
verify_callable=lambda: _run_verify_once(path=path, sprint=sprint),
|
|
1100
|
+
)
|
|
1101
|
+
click.echo(f"Repair outcome: {outcome.status}")
|
|
1102
|
+
click.echo(f"Repair history: {_display_path(outcome.history_session_dir, project_root)}")
|
|
1103
|
+
raise SystemExit(_repair_exit_code(outcome.status))
|
|
1059
1104
|
|
|
1060
1105
|
|
|
1061
1106
|
def _run_e2e_generate(path: str, base_url: str, output: str | None, framework: str, mode: str = "scenarios") -> None:
|
|
@@ -2396,6 +2441,358 @@ def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
|
|
|
2396
2441
|
return {}
|
|
2397
2442
|
|
|
2398
2443
|
|
|
2444
|
+
def _run_verify_once(path: str, sprint: int | None = None) -> _CliVerificationResult:
|
|
2445
|
+
try:
|
|
2446
|
+
_run_pro_command("verify", path=path, sprint=sprint)
|
|
2447
|
+
except SystemExit as exc:
|
|
2448
|
+
exit_code = _system_exit_code(exc)
|
|
2449
|
+
passed = exit_code == 0
|
|
2450
|
+
failure = None
|
|
2451
|
+
if not passed:
|
|
2452
|
+
failure = _verification_failure_report(
|
|
2453
|
+
"verify",
|
|
2454
|
+
[],
|
|
2455
|
+
[f"codd verify exited with code {exit_code}"],
|
|
2456
|
+
{},
|
|
2457
|
+
)
|
|
2458
|
+
return _CliVerificationResult(passed=passed, exit_code=exit_code, failure=failure)
|
|
2459
|
+
return _CliVerificationResult(passed=True, exit_code=0, failure=None)
|
|
2460
|
+
|
|
2461
|
+
|
|
2462
|
+
def _system_exit_code(exc: SystemExit) -> int:
|
|
2463
|
+
code = exc.code
|
|
2464
|
+
if code is None:
|
|
2465
|
+
return 0
|
|
2466
|
+
try:
|
|
2467
|
+
return int(code)
|
|
2468
|
+
except (TypeError, ValueError):
|
|
2469
|
+
return 1
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
def _load_required_repair_config(project_root: Path) -> dict[str, Any] | None:
|
|
2473
|
+
try:
|
|
2474
|
+
config = load_project_config(project_root)
|
|
2475
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2476
|
+
click.echo(f"WARN: codd.yaml repair config is required for repair: {exc}")
|
|
2477
|
+
return None
|
|
2478
|
+
|
|
2479
|
+
repair = config.get("repair")
|
|
2480
|
+
if not isinstance(repair, dict) or not repair:
|
|
2481
|
+
click.echo("WARN: codd.yaml [repair] section is required for repair.")
|
|
2482
|
+
return None
|
|
2483
|
+
return config
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
def _run_repair_loop(
|
|
2487
|
+
project_root: Path,
|
|
2488
|
+
failure: Any,
|
|
2489
|
+
*,
|
|
2490
|
+
repair_config: dict[str, Any],
|
|
2491
|
+
max_attempts: int | None,
|
|
2492
|
+
engine_name: str | None,
|
|
2493
|
+
verify_callable,
|
|
2494
|
+
):
|
|
2495
|
+
from codd.dag import DAG
|
|
2496
|
+
from codd.dag.builder import build_dag
|
|
2497
|
+
from codd.repair import RepairLoop, RepairLoopConfig
|
|
2498
|
+
|
|
2499
|
+
try:
|
|
2500
|
+
dag = build_dag(project_root)
|
|
2501
|
+
except (FileNotFoundError, ValueError):
|
|
2502
|
+
dag = DAG()
|
|
2503
|
+
|
|
2504
|
+
resolved_failure = failure if failure is not None else _verification_failure_report("verify", [], [], {})
|
|
2505
|
+
if getattr(resolved_failure, "dag_snapshot", None) in ({}, None):
|
|
2506
|
+
resolved_failure.dag_snapshot = _dag_snapshot(dag, project_root)
|
|
2507
|
+
|
|
2508
|
+
repair = repair_config.get("repair") if isinstance(repair_config.get("repair"), dict) else {}
|
|
2509
|
+
config = RepairLoopConfig(
|
|
2510
|
+
max_attempts=_repair_max_attempts(repair, max_attempts),
|
|
2511
|
+
approval_mode=str(repair.get("approval_mode") or "required"), # type: ignore[arg-type]
|
|
2512
|
+
history_dir=Path(str(repair.get("history_dir") or ".codd/repair_history")),
|
|
2513
|
+
engine_name=str(engine_name or repair.get("engine_name") or repair.get("engine") or "llm"),
|
|
2514
|
+
)
|
|
2515
|
+
return RepairLoop(config, project_root).run(resolved_failure, dag, verify_callable=verify_callable)
|
|
2516
|
+
|
|
2517
|
+
|
|
2518
|
+
def _repair_max_attempts(repair: dict[str, Any], max_attempts: int | None) -> int:
|
|
2519
|
+
raw = max_attempts if max_attempts is not None else repair.get("max_attempts", 3)
|
|
2520
|
+
try:
|
|
2521
|
+
return max(1, int(raw))
|
|
2522
|
+
except (TypeError, ValueError):
|
|
2523
|
+
return 3
|
|
2524
|
+
|
|
2525
|
+
|
|
2526
|
+
def _dag_snapshot(dag: Any, project_root: Path) -> dict[str, Any]:
|
|
2527
|
+
try:
|
|
2528
|
+
from codd.dag.builder import dag_to_dict
|
|
2529
|
+
|
|
2530
|
+
return dag_to_dict(dag, project_root)
|
|
2531
|
+
except Exception: # noqa: BLE001 - repair reports should survive DAG serialization failures.
|
|
2532
|
+
return {}
|
|
2533
|
+
|
|
2534
|
+
|
|
2535
|
+
def _verification_failure_report(
|
|
2536
|
+
check_name: str,
|
|
2537
|
+
failed_nodes: list[str],
|
|
2538
|
+
error_messages: list[str],
|
|
2539
|
+
dag_snapshot: dict[str, Any],
|
|
2540
|
+
):
|
|
2541
|
+
from codd.repair.schema import VerificationFailureReport
|
|
2542
|
+
|
|
2543
|
+
return VerificationFailureReport(
|
|
2544
|
+
check_name=check_name,
|
|
2545
|
+
failed_nodes=failed_nodes,
|
|
2546
|
+
error_messages=error_messages,
|
|
2547
|
+
dag_snapshot=dag_snapshot,
|
|
2548
|
+
timestamp=_utc_timestamp(),
|
|
2549
|
+
)
|
|
2550
|
+
|
|
2551
|
+
|
|
2552
|
+
def _utc_timestamp() -> str:
|
|
2553
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
def _repair_exit_code(status: str) -> int:
|
|
2557
|
+
return {
|
|
2558
|
+
"REPAIR_SUCCESS": 0,
|
|
2559
|
+
"REPAIR_REJECTED_BY_HITL": 1,
|
|
2560
|
+
"REPAIR_EXHAUSTED": 2,
|
|
2561
|
+
"REPAIR_FAILED": 3,
|
|
2562
|
+
}.get(str(status), 3)
|
|
2563
|
+
|
|
2564
|
+
|
|
2565
|
+
def _load_failure_report(path: Path) -> Any:
|
|
2566
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
2567
|
+
if not isinstance(payload, dict):
|
|
2568
|
+
raise ValueError("failure report must contain a YAML mapping")
|
|
2569
|
+
return _verification_failure_report(
|
|
2570
|
+
str(payload.get("check_name") or payload.get("name") or "verify"),
|
|
2571
|
+
_repair_string_list(payload.get("failed_nodes") or payload.get("nodes")),
|
|
2572
|
+
_repair_string_list(payload.get("error_messages") or payload.get("errors") or payload.get("messages")),
|
|
2573
|
+
payload.get("dag_snapshot") if isinstance(payload.get("dag_snapshot"), dict) else {},
|
|
2574
|
+
)
|
|
2575
|
+
|
|
2576
|
+
|
|
2577
|
+
def _repair_string_list(value: Any) -> list[str]:
|
|
2578
|
+
if isinstance(value, list):
|
|
2579
|
+
return [str(item) for item in value if item is not None]
|
|
2580
|
+
if value is None:
|
|
2581
|
+
return []
|
|
2582
|
+
return [str(value)]
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
def _repair_history_dir(project_root: Path, config: dict[str, Any] | None = None) -> Path:
|
|
2586
|
+
repair = config.get("repair") if isinstance(config, dict) and isinstance(config.get("repair"), dict) else {}
|
|
2587
|
+
history_dir = Path(str(repair.get("history_dir") or ".codd/repair_history"))
|
|
2588
|
+
if history_dir.is_absolute():
|
|
2589
|
+
return history_dir
|
|
2590
|
+
return project_root / history_dir
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
def _session_attempt_dirs(session_dir: Path) -> list[Path]:
|
|
2594
|
+
return sorted(
|
|
2595
|
+
[path for path in session_dir.glob("attempt_*") if path.is_dir()],
|
|
2596
|
+
key=lambda path: _attempt_number(path.name),
|
|
2597
|
+
)
|
|
2598
|
+
|
|
2599
|
+
|
|
2600
|
+
def _attempt_number(name: str) -> int:
|
|
2601
|
+
try:
|
|
2602
|
+
return int(name.removeprefix("attempt_"))
|
|
2603
|
+
except ValueError:
|
|
2604
|
+
return 10**9
|
|
2605
|
+
|
|
2606
|
+
|
|
2607
|
+
def _read_repair_yaml(path: Path) -> Any:
|
|
2608
|
+
if not path.exists():
|
|
2609
|
+
return None
|
|
2610
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
def _repair_session_summary(session_dir: Path) -> dict[str, Any]:
|
|
2614
|
+
final_status = _read_repair_yaml(session_dir / "final_status.yaml") or {}
|
|
2615
|
+
attempts = _session_attempt_dirs(session_dir)
|
|
2616
|
+
return {
|
|
2617
|
+
"history_id": session_dir.name,
|
|
2618
|
+
"timestamp": final_status.get("timestamp") or session_dir.name,
|
|
2619
|
+
"status": final_status.get("outcome") or final_status.get("status") or "IN_PROGRESS",
|
|
2620
|
+
"attempts": len(attempts),
|
|
2621
|
+
"path": str(session_dir),
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
|
|
2625
|
+
def _session_matches_design_doc(session_dir: Path, design_doc: str | None) -> bool:
|
|
2626
|
+
if not design_doc:
|
|
2627
|
+
return True
|
|
2628
|
+
needle = str(design_doc)
|
|
2629
|
+
for path in session_dir.glob("**/*.yaml"):
|
|
2630
|
+
try:
|
|
2631
|
+
if needle in path.read_text(encoding="utf-8"):
|
|
2632
|
+
return True
|
|
2633
|
+
except OSError:
|
|
2634
|
+
continue
|
|
2635
|
+
return False
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
def _resolve_history_session(project_root: Path, history_id: str, config: dict[str, Any] | None = None) -> Path:
|
|
2639
|
+
raw = Path(history_id).expanduser()
|
|
2640
|
+
if raw.is_absolute() and raw.is_dir():
|
|
2641
|
+
return raw
|
|
2642
|
+
if raw.is_dir():
|
|
2643
|
+
return raw.resolve()
|
|
2644
|
+
session_dir = _repair_history_dir(project_root, config) / history_id
|
|
2645
|
+
if session_dir.is_dir():
|
|
2646
|
+
return session_dir
|
|
2647
|
+
raise FileNotFoundError(f"repair history session not found: {history_id}")
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
def _load_repair_proposal(path: Path) -> Any:
|
|
2651
|
+
from codd.repair.schema import RepairProposal
|
|
2652
|
+
|
|
2653
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
2654
|
+
if not isinstance(payload, dict):
|
|
2655
|
+
raise ValueError("repair proposal must contain a YAML mapping")
|
|
2656
|
+
return RepairProposal(**payload)
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
@main.group(invoke_without_command=True)
|
|
2660
|
+
@click.option("--from-report", "from_report", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
2661
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2662
|
+
@click.option("--max-attempts", default=None, type=click.IntRange(min=1), help="Maximum repair attempts")
|
|
2663
|
+
@click.option("--engine", "engine_name", default=None, help="Repair engine name")
|
|
2664
|
+
@click.pass_context
|
|
2665
|
+
def repair(ctx, from_report: Path | None, project_path: str, max_attempts: int | None, engine_name: str | None):
|
|
2666
|
+
"""Run and inspect repair sessions."""
|
|
2667
|
+
if ctx.invoked_subcommand is not None:
|
|
2668
|
+
return
|
|
2669
|
+
if from_report is None:
|
|
2670
|
+
click.echo(ctx.get_help())
|
|
2671
|
+
return
|
|
2672
|
+
|
|
2673
|
+
project_root = Path(project_path).resolve()
|
|
2674
|
+
repair_config = _load_required_repair_config(project_root)
|
|
2675
|
+
if repair_config is None:
|
|
2676
|
+
raise SystemExit(1)
|
|
2677
|
+
|
|
2678
|
+
try:
|
|
2679
|
+
failure = _load_failure_report(from_report)
|
|
2680
|
+
outcome = _run_repair_loop(
|
|
2681
|
+
project_root,
|
|
2682
|
+
failure,
|
|
2683
|
+
repair_config=repair_config,
|
|
2684
|
+
max_attempts=max_attempts,
|
|
2685
|
+
engine_name=engine_name,
|
|
2686
|
+
verify_callable=lambda: _run_verify_once(path=str(project_root), sprint=None),
|
|
2687
|
+
)
|
|
2688
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2689
|
+
click.echo(f"Error: {exc}")
|
|
2690
|
+
raise SystemExit(1)
|
|
2691
|
+
|
|
2692
|
+
click.echo(f"Repair outcome: {outcome.status}")
|
|
2693
|
+
click.echo(f"Repair history: {_display_path(outcome.history_session_dir, project_root)}")
|
|
2694
|
+
raise SystemExit(_repair_exit_code(outcome.status))
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
@repair.command("history")
|
|
2698
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2699
|
+
@click.option("--last", "last", default=10, type=click.IntRange(min=1), show_default=True, help="Number of sessions")
|
|
2700
|
+
@click.option("--design-doc", "design_doc", default=None, help="Filter sessions containing a design doc path")
|
|
2701
|
+
def repair_history(project_path: str, last: int, design_doc: str | None):
|
|
2702
|
+
"""List repair history sessions."""
|
|
2703
|
+
from codd.repair.history import RepairHistory
|
|
2704
|
+
|
|
2705
|
+
project_root = Path(project_path).resolve()
|
|
2706
|
+
config = _load_optional_project_config(project_root)
|
|
2707
|
+
sessions = [
|
|
2708
|
+
session
|
|
2709
|
+
for session in RepairHistory().list_sessions(_repair_history_dir(project_root, config))
|
|
2710
|
+
if _session_matches_design_doc(session, design_doc)
|
|
2711
|
+
][:last]
|
|
2712
|
+
|
|
2713
|
+
if not sessions:
|
|
2714
|
+
click.echo("No repair history found.")
|
|
2715
|
+
return
|
|
2716
|
+
|
|
2717
|
+
for session_dir in sessions:
|
|
2718
|
+
summary = _repair_session_summary(session_dir)
|
|
2719
|
+
click.echo(
|
|
2720
|
+
f"{summary['history_id']}\t{summary['status']}\t"
|
|
2721
|
+
f"attempts={summary['attempts']}\t{summary['timestamp']}"
|
|
2722
|
+
)
|
|
2723
|
+
|
|
2724
|
+
|
|
2725
|
+
@repair.command("approve")
|
|
2726
|
+
@click.argument("history_id")
|
|
2727
|
+
@click.option("--attempt", "attempt", default=None, type=click.IntRange(min=0), help="Attempt number")
|
|
2728
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2729
|
+
def repair_approve(history_id: str, attempt: int | None, project_path: str):
|
|
2730
|
+
"""Approve a repair proposal in history."""
|
|
2731
|
+
from codd.repair.approval_repair import approve_repair_proposal
|
|
2732
|
+
|
|
2733
|
+
project_root = Path(project_path).resolve()
|
|
2734
|
+
config = _load_optional_project_config(project_root)
|
|
2735
|
+
try:
|
|
2736
|
+
session_dir = _resolve_history_session(project_root, history_id, config)
|
|
2737
|
+
attempt_dirs = _session_attempt_dirs(session_dir)
|
|
2738
|
+
if not attempt_dirs:
|
|
2739
|
+
raise FileNotFoundError("repair attempt not found")
|
|
2740
|
+
attempt_dir = session_dir / f"attempt_{attempt}" if attempt is not None else attempt_dirs[-1]
|
|
2741
|
+
proposal = _load_repair_proposal(attempt_dir / "repair_proposal.yaml")
|
|
2742
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2743
|
+
click.echo(f"Error: {exc}")
|
|
2744
|
+
raise SystemExit(1)
|
|
2745
|
+
|
|
2746
|
+
approval_config = dict(config)
|
|
2747
|
+
repair_config = dict(approval_config.get("repair") if isinstance(approval_config.get("repair"), dict) else {})
|
|
2748
|
+
repair_config["approval_decision"] = "approved"
|
|
2749
|
+
approval_config["repair"] = repair_config
|
|
2750
|
+
approved = approve_repair_proposal(
|
|
2751
|
+
proposal,
|
|
2752
|
+
approval_mode="required",
|
|
2753
|
+
codd_yaml=approval_config,
|
|
2754
|
+
notify_callable=click.echo,
|
|
2755
|
+
)
|
|
2756
|
+
if not approved:
|
|
2757
|
+
click.echo("Repair proposal not approved.")
|
|
2758
|
+
raise SystemExit(1)
|
|
2759
|
+
|
|
2760
|
+
(attempt_dir / "approval.yaml").write_text(
|
|
2761
|
+
yaml.safe_dump({"status": "approved", "timestamp": _utc_timestamp()}, sort_keys=False),
|
|
2762
|
+
encoding="utf-8",
|
|
2763
|
+
)
|
|
2764
|
+
click.echo(f"Approved repair proposal: {session_dir.name} attempt={attempt_dir.name.removeprefix('attempt_')}")
|
|
2765
|
+
|
|
2766
|
+
|
|
2767
|
+
@repair.command("status")
|
|
2768
|
+
@click.argument("history_id", required=False)
|
|
2769
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2770
|
+
def repair_status(history_id: str | None, project_path: str):
|
|
2771
|
+
"""Show repair session status."""
|
|
2772
|
+
from codd.repair.history import RepairHistory
|
|
2773
|
+
|
|
2774
|
+
project_root = Path(project_path).resolve()
|
|
2775
|
+
config = _load_optional_project_config(project_root)
|
|
2776
|
+
try:
|
|
2777
|
+
if history_id is None:
|
|
2778
|
+
sessions = RepairHistory().list_sessions(_repair_history_dir(project_root, config))
|
|
2779
|
+
if not sessions:
|
|
2780
|
+
click.echo("No repair history found.")
|
|
2781
|
+
raise SystemExit(1)
|
|
2782
|
+
session_dir = sessions[0]
|
|
2783
|
+
else:
|
|
2784
|
+
session_dir = _resolve_history_session(project_root, history_id, config)
|
|
2785
|
+
except FileNotFoundError as exc:
|
|
2786
|
+
click.echo(f"Error: {exc}")
|
|
2787
|
+
raise SystemExit(1)
|
|
2788
|
+
|
|
2789
|
+
summary = _repair_session_summary(session_dir)
|
|
2790
|
+
click.echo(f"history_id: {summary['history_id']}")
|
|
2791
|
+
click.echo(f"status: {summary['status']}")
|
|
2792
|
+
click.echo(f"attempts: {summary['attempts']}")
|
|
2793
|
+
click.echo(f"timestamp: {summary['timestamp']}")
|
|
2794
|
+
|
|
2795
|
+
|
|
2399
2796
|
@main.group()
|
|
2400
2797
|
def dag():
|
|
2401
2798
|
"""DAG Completeness Gate commands."""
|
|
@@ -6,6 +6,14 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def reset_dag_cache(project_root=None) -> None:
|
|
10
|
+
"""Clear DAG builder cache state for a fresh verification attempt."""
|
|
11
|
+
|
|
12
|
+
from codd.dag.builder import reset_dag_cache as _reset_dag_cache
|
|
13
|
+
|
|
14
|
+
_reset_dag_cache(project_root)
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
@dataclass
|
|
10
18
|
class Node:
|
|
11
19
|
id: str
|
|
@@ -30,6 +30,7 @@ from codd.deployment.extractor import (
|
|
|
30
30
|
LOGGER = logging.getLogger(__name__)
|
|
31
31
|
DEFAULTS_DIR = Path(__file__).parent / "defaults"
|
|
32
32
|
DEFAULT_PROJECT_TYPE = "generic"
|
|
33
|
+
_DAG_BUILD_CACHE: dict[str, Any] = {}
|
|
33
34
|
LEGACY_IMPLEMENTATION_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java")
|
|
34
35
|
LEGACY_TEST_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".bats")
|
|
35
36
|
PLAN_HEADER_RE = re.compile(r"^#{2,6}\s+([A-Za-z0-9]+(?:[-_.][A-Za-z0-9]+)*)(?:\s+(.+))?$", re.MULTILINE)
|
|
@@ -85,6 +86,19 @@ def build_dag(project_root: Path, settings: dict[str, Any] | None = None) -> DAG
|
|
|
85
86
|
return dag
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
def reset_dag_cache(project_root: Path | None = None) -> None:
|
|
90
|
+
"""Clear in-process DAG builder cache state.
|
|
91
|
+
|
|
92
|
+
The builder currently rebuilds eagerly, but repair verification calls this
|
|
93
|
+
public hook per attempt so future memoization cannot leak stale DAG state.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if project_root is None:
|
|
97
|
+
_DAG_BUILD_CACHE.clear()
|
|
98
|
+
return
|
|
99
|
+
_DAG_BUILD_CACHE.pop(str(Path(project_root).resolve()), None)
|
|
100
|
+
|
|
101
|
+
|
|
88
102
|
def load_dag_settings(project_root: Path, settings: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
89
103
|
"""Load project-type defaults and apply ``codd.yaml dag:`` overrides."""
|
|
90
104
|
|
|
@@ -14,7 +14,7 @@ class VerificationMeansCatalog:
|
|
|
14
14
|
"""Load verification means from project override or bundled defaults."""
|
|
15
15
|
|
|
16
16
|
DEFAULT_CATALOG_PATH: ClassVar[str] = str(
|
|
17
|
-
Path(__file__).parents[
|
|
17
|
+
Path(__file__).parents[3] / "llm" / "templates" / "verification_means_catalog.yaml"
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
@classmethod
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Repair engine package."""
|
|
2
|
+
|
|
3
|
+
from codd.repair.llm_repair_engine import LlmRepairEngine, RepairFailed
|
|
4
|
+
from codd.repair.loop import RepairLoop, RepairLoopConfig, RepairLoopOutcome
|
|
5
|
+
from codd.repair.verify_runner import VerificationResult, VerifyRunner
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LlmRepairEngine",
|
|
9
|
+
"RepairFailed",
|
|
10
|
+
"RepairLoop",
|
|
11
|
+
"RepairLoopConfig",
|
|
12
|
+
"RepairLoopOutcome",
|
|
13
|
+
"VerificationResult",
|
|
14
|
+
"VerifyRunner",
|
|
15
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Approval helpers for repair proposals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Any, Callable, Literal, Mapping
|
|
9
|
+
|
|
10
|
+
from codd.repair.schema import RepairProposal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RepairApprovalMode = Literal["required", "auto", "per_attempt"]
|
|
14
|
+
APPROVAL_MODES: set[str] = {"required", "auto", "per_attempt"}
|
|
15
|
+
DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL = 5
|
|
16
|
+
|
|
17
|
+
_APPROVE_VALUES = {"1", "approve", "approved", "true", "y", "yes"}
|
|
18
|
+
_REJECT_VALUES = {"0", "false", "n", "no", "reject", "rejected", "skip", "skipped"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RepairApprovalError(RuntimeError):
|
|
22
|
+
"""Raised when a repair proposal cannot pass the approval policy."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def approve_repair_proposal(
|
|
26
|
+
proposal: RepairProposal,
|
|
27
|
+
*,
|
|
28
|
+
approval_mode: RepairApprovalMode,
|
|
29
|
+
codd_yaml: dict,
|
|
30
|
+
notify_callable: Callable[[str], None] | None = None,
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Return whether a repair proposal may be applied."""
|
|
33
|
+
|
|
34
|
+
mode = _approval_mode(approval_mode)
|
|
35
|
+
repair_config = _repair_config(codd_yaml)
|
|
36
|
+
|
|
37
|
+
if mode == "auto":
|
|
38
|
+
allow_auto = _mapping(repair_config.get("allow_auto"))
|
|
39
|
+
if not bool(allow_auto.get("require_explicit_optin")):
|
|
40
|
+
raise RepairApprovalError(
|
|
41
|
+
"auto repair approval requires repair.allow_auto.require_explicit_optin=true"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
max_files = _max_files_per_auto_proposal(allow_auto)
|
|
45
|
+
if len(proposal.patches) <= max_files:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"repair proposal exceeds repair.allow_auto.max_files_per_proposal; "
|
|
50
|
+
"escalating to required approval",
|
|
51
|
+
RuntimeWarning,
|
|
52
|
+
stacklevel=2,
|
|
53
|
+
)
|
|
54
|
+
mode = "required"
|
|
55
|
+
|
|
56
|
+
message = _approval_message(proposal, mode)
|
|
57
|
+
if notify_callable is not None:
|
|
58
|
+
notify_callable(message)
|
|
59
|
+
else:
|
|
60
|
+
print(message)
|
|
61
|
+
|
|
62
|
+
decision = _configured_decision(repair_config) or os.environ.get("CODD_REPAIR_APPROVAL")
|
|
63
|
+
if decision is not None:
|
|
64
|
+
return _decision_to_bool(decision)
|
|
65
|
+
|
|
66
|
+
if sys.stdin.isatty():
|
|
67
|
+
try:
|
|
68
|
+
return _decision_to_bool(input("Approve repair proposal? [y/N]: "))
|
|
69
|
+
except EOFError:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _approval_mode(value: str) -> RepairApprovalMode:
|
|
76
|
+
text = str(value or "required")
|
|
77
|
+
return text if text in APPROVAL_MODES else "required" # type: ignore[return-value]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _repair_config(codd_yaml: Mapping[str, Any] | None) -> Mapping[str, Any]:
|
|
81
|
+
return _mapping(codd_yaml.get("repair") if isinstance(codd_yaml, Mapping) else None)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _mapping(value: Any) -> Mapping[str, Any]:
|
|
85
|
+
return value if isinstance(value, Mapping) else {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _max_files_per_auto_proposal(allow_auto: Mapping[str, Any]) -> int:
|
|
89
|
+
raw = allow_auto.get("max_files_per_proposal", DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL)
|
|
90
|
+
try:
|
|
91
|
+
return max(0, int(raw))
|
|
92
|
+
except (TypeError, ValueError):
|
|
93
|
+
return DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _configured_decision(repair_config: Mapping[str, Any]) -> Any:
|
|
97
|
+
for key in ("approval_decision", "approval_response"):
|
|
98
|
+
value = repair_config.get(key)
|
|
99
|
+
if value is not None:
|
|
100
|
+
return value
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _decision_to_bool(value: Any) -> bool:
|
|
105
|
+
text = str(value or "").strip().lower()
|
|
106
|
+
if text in _APPROVE_VALUES:
|
|
107
|
+
return True
|
|
108
|
+
if text in _REJECT_VALUES:
|
|
109
|
+
return False
|
|
110
|
+
raise RepairApprovalError(f"unknown repair approval decision: {value}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _approval_message(proposal: RepairProposal, mode: RepairApprovalMode) -> str:
|
|
114
|
+
files = ", ".join(patch.file_path for patch in proposal.patches) or "none"
|
|
115
|
+
return (
|
|
116
|
+
"CoDD repair approval required "
|
|
117
|
+
f"(mode={mode}, patches={len(proposal.patches)}, files={files})"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"APPROVAL_MODES",
|
|
123
|
+
"DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL",
|
|
124
|
+
"RepairApprovalError",
|
|
125
|
+
"RepairApprovalMode",
|
|
126
|
+
"approve_repair_proposal",
|
|
127
|
+
]
|
|
@@ -56,8 +56,9 @@ class RepairHistory:
|
|
|
56
56
|
def finalize(self, session_dir: Path, outcome: str) -> None:
|
|
57
57
|
"""Write the final repair session outcome."""
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
allowed = {"REPAIR_SUCCESS", "REPAIR_EXHAUSTED", "REPAIR_REJECTED_BY_HITL", "REPAIR_FAILED"}
|
|
60
|
+
if outcome not in allowed:
|
|
61
|
+
raise ValueError(f"outcome must be one of {sorted(allowed)}")
|
|
61
62
|
_write_yaml(
|
|
62
63
|
Path(session_dir) / "final_status.yaml",
|
|
63
64
|
{"outcome": outcome, "timestamp": _timestamp()},
|