codd-dev 1.25.0__tar.gz → 1.26.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.25.0 → codd_dev-1.26.0}/PKG-INFO +2 -1
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/cli.py +234 -4
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/builder.py +174 -13
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/task_completion.py +3 -1
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/user_journey_coherence.py +95 -1
- codd_dev-1.26.0/codd/dag/defaults/cpp_embedded.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/csharp.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/elixir.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/generic.yaml +5 -0
- codd_dev-1.26.0/codd/dag/defaults/java.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/kotlin.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/ruby.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/rust.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/scala.yaml +2 -0
- codd_dev-1.26.0/codd/dag/defaults/swift.yaml +2 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/defaults/web.yaml +2 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/extractor.py +12 -0
- codd_dev-1.26.0/codd/deployment/defaults/verification_means_catalog.yaml +6 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/extractor.py +129 -3
- codd_dev-1.26.0/codd/deployment/providers/ai_command.py +186 -0
- codd_dev-1.26.0/codd/deployment/providers/llm_consideration.py +338 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/__init__.py +21 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/assertion_handlers.py +244 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/cdp_browser.py +350 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/cdp_engines.py +40 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/cdp_launchers.py +44 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/cdp_wire.py +130 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/form_strategies.py +44 -0
- codd_dev-1.26.0/codd/deployment/providers/verification/means_catalog.py +102 -0
- codd_dev-1.26.0/codd/llm/approval.py +324 -0
- codd_dev-1.26.0/codd/llm/means_catalog_loader.py +122 -0
- codd_dev-1.26.0/codd/llm/parser.py +119 -0
- codd_dev-1.26.0/codd/llm/prompt_builder.py +57 -0
- codd_dev-1.26.0/codd/llm/strategy_validator.py +37 -0
- codd_dev-1.26.0/codd/llm/templates/meta_instruction.md +37 -0
- codd_dev-1.26.0/codd/llm/templates/verification_means_catalog.yaml +6 -0
- codd_dev-1.26.0/codd/repair/__init__.py +5 -0
- codd_dev-1.26.0/codd/repair/engine.py +90 -0
- codd_dev-1.26.0/codd/repair/git_patcher.py +120 -0
- codd_dev-1.26.0/codd/repair/history.py +124 -0
- codd_dev-1.26.0/codd/repair/llm_repair_engine.py +305 -0
- codd_dev-1.26.0/codd/repair/schema.py +85 -0
- codd_dev-1.26.0/codd/repair/templates/analyze_meta.md +28 -0
- codd_dev-1.26.0/codd/repair/templates/propose_meta.md +34 -0
- codd_dev-1.26.0/codd/templates/extract_ai_prompt_baseline.md +418 -0
- codd_dev-1.26.0/codd/templates/lexicon_questions.md +151 -0
- codd_dev-1.26.0/docs/cookbook/cdp_browser/README.md +88 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/pyproject.toml +3 -1
- {codd_dev-1.25.0 → codd_dev-1.26.0}/.gitignore +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/LICENSE +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/README.md +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/__main__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/_git_helper.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/ask_user_question_adapter.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/assembler.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/bridge.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/clustering.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/coherence_adapters.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/coherence_engine.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/config.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/contracts.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/coverage_auditor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/coverage_metrics.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/depends_on_consistency.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/deployment_completeness.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/edge_validity.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/node_completeness.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/checks/transitive_closure.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/defaults/cli.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/defaults/iot.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/defaults/mobile.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/dag/runner.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/defaults.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deploy_targets/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deploy_targets/app_service.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deploy_targets/base.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deploy_targets/docker_compose.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployer.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/checks/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/defaults/runtime_capability_inference.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/schema/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/schema/prisma.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/target/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/target/docker_compose.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/verification/curl.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/deployment/providers/verification/playwright.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/design_md.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/drift.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/e2e_extractor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/e2e_generator.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/e2e_runner.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/env_refs.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/extract_ai.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/extractor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixer.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixup_drift.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/generator.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/graph.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hitl_session.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/pre-commit +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/recipes/codex_hook.sh +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/implementer.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/inheritance.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/knowledge_fetcher.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/lexicon.py +0 -0
- {codd_dev-1.25.0/codd/deployment/providers/verification → codd_dev-1.26.0/codd/llm}/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/mcp_server.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/measure.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/parsing.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/planner.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/policy.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/preflight/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/preflight/defaults/cli.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/preflight/defaults/iot.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/preflight/defaults/mobile.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/preflight/defaults/web.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/propagate.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/propagator.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/registry.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/repair_slice.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/require.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/require_plugins.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/require_propagate.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/web.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/required_artifacts_deriver.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/requirement_completeness_auditor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/restore.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/routes_extractor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/scanner.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/schema_refs.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/screen_flow_validator.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/screen_transition_extractor.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/screen_transitions/defaults.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/synth.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/lexicon_schema.yaml +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/traceability.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/validator.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/__init__.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/events.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/propagation_log.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/propagation_pipeline.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/test_runner.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/watch/watcher.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.0}/codd/wiring.py +0 -0
- {codd_dev-1.25.0 → codd_dev-1.26.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.26.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
|
|
@@ -20,6 +20,7 @@ Requires-Dist: jinja2>=3.1.0
|
|
|
20
20
|
Requires-Dist: pyyaml>=6.0
|
|
21
21
|
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
22
22
|
Requires-Dist: watchdog>=4.0.0
|
|
23
|
+
Requires-Dist: websocket-client>=1.8.0
|
|
23
24
|
Provides-Extra: ai
|
|
24
25
|
Provides-Extra: api-parsers
|
|
25
26
|
Requires-Dist: graphql-core>=3.2.0; extra == 'api-parsers'
|
|
@@ -2283,6 +2283,119 @@ def test_cmd(project_path: str, related: tuple[str, ...], dry_run: bool):
|
|
|
2283
2283
|
raise SystemExit(1)
|
|
2284
2284
|
|
|
2285
2285
|
|
|
2286
|
+
@main.group()
|
|
2287
|
+
def llm():
|
|
2288
|
+
"""Manage LLM-derived considerations."""
|
|
2289
|
+
pass
|
|
2290
|
+
|
|
2291
|
+
|
|
2292
|
+
@llm.command("derive")
|
|
2293
|
+
@click.argument("design_doc", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
2294
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2295
|
+
@click.option("--ai-cmd", default=None, help="Override AI CLI command")
|
|
2296
|
+
@click.option("--model", default=None, help="Override AI model")
|
|
2297
|
+
@click.option("--force", is_flag=True, help="Bypass cached derived considerations")
|
|
2298
|
+
def llm_derive(design_doc: Path, project_path: str, ai_cmd: str | None, model: str | None, force: bool):
|
|
2299
|
+
"""Derive considerations for a design document."""
|
|
2300
|
+
from codd.deployment.providers.ai_command import SubprocessAiCommand
|
|
2301
|
+
from codd.deployment.providers.llm_consideration import LlmConsiderationProvider
|
|
2302
|
+
from codd.llm.approval import notify_pending_considerations
|
|
2303
|
+
|
|
2304
|
+
project_root = Path(project_path).resolve()
|
|
2305
|
+
config = _load_optional_project_config(project_root)
|
|
2306
|
+
provider = LlmConsiderationProvider(
|
|
2307
|
+
SubprocessAiCommand(command=ai_cmd, project_root=project_root, config=config),
|
|
2308
|
+
project_root=project_root,
|
|
2309
|
+
cache_dir=project_root / ".codd" / "consideration_cache",
|
|
2310
|
+
model=model,
|
|
2311
|
+
use_cache=not force,
|
|
2312
|
+
)
|
|
2313
|
+
result = provider.provide(design_doc.read_text(encoding="utf-8"), {"model": model} if model else {})
|
|
2314
|
+
notify_pending_considerations(result.considerations, config)
|
|
2315
|
+
click.echo(f"Derived considerations: {len(result.considerations)}")
|
|
2316
|
+
|
|
2317
|
+
|
|
2318
|
+
@llm.command("approve")
|
|
2319
|
+
@click.argument("consideration_id", required=False)
|
|
2320
|
+
@click.option("--all", "approve_all", is_flag=True, help="Approve all pending considerations")
|
|
2321
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2322
|
+
def llm_approve(consideration_id: str | None, approve_all: bool, project_path: str):
|
|
2323
|
+
"""Approve one or all pending considerations."""
|
|
2324
|
+
from codd.llm.approval import ApprovalCache, consideration_status, load_cached_considerations
|
|
2325
|
+
|
|
2326
|
+
project_root = Path(project_path).resolve()
|
|
2327
|
+
considerations = load_cached_considerations(project_root)
|
|
2328
|
+
by_id = {consideration.id: consideration for consideration in considerations}
|
|
2329
|
+
|
|
2330
|
+
if approve_all:
|
|
2331
|
+
targets = [
|
|
2332
|
+
consideration
|
|
2333
|
+
for consideration in considerations
|
|
2334
|
+
if consideration_status(consideration, project_root) == "pending"
|
|
2335
|
+
]
|
|
2336
|
+
else:
|
|
2337
|
+
if not consideration_id:
|
|
2338
|
+
click.echo("Error: consideration_id or --all is required")
|
|
2339
|
+
raise SystemExit(2)
|
|
2340
|
+
if consideration_id not in by_id:
|
|
2341
|
+
click.echo(f"Error: consideration not found: {consideration_id}")
|
|
2342
|
+
raise SystemExit(1)
|
|
2343
|
+
targets = [by_id[consideration_id]]
|
|
2344
|
+
|
|
2345
|
+
for consideration in targets:
|
|
2346
|
+
ApprovalCache.save(consideration.id, "approved", project_root)
|
|
2347
|
+
click.echo(f"Approved considerations: {len(targets)}")
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
@llm.command("skip")
|
|
2351
|
+
@click.argument("consideration_id")
|
|
2352
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2353
|
+
def llm_skip(consideration_id: str, project_path: str):
|
|
2354
|
+
"""Skip one consideration."""
|
|
2355
|
+
from codd.llm.approval import ApprovalCache, load_cached_considerations
|
|
2356
|
+
|
|
2357
|
+
project_root = Path(project_path).resolve()
|
|
2358
|
+
known = {consideration.id for consideration in load_cached_considerations(project_root)}
|
|
2359
|
+
if consideration_id not in known:
|
|
2360
|
+
click.echo(f"Error: consideration not found: {consideration_id}")
|
|
2361
|
+
raise SystemExit(1)
|
|
2362
|
+
ApprovalCache.save(consideration_id, "skipped", project_root)
|
|
2363
|
+
click.echo(f"Skipped consideration: {consideration_id}")
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
@llm.command("list")
|
|
2367
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2368
|
+
@click.option("--format", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
2369
|
+
def llm_list(project_path: str, output_format: str):
|
|
2370
|
+
"""List generated considerations with approval status."""
|
|
2371
|
+
from codd.llm.approval import consideration_status, consideration_to_dict, load_cached_considerations
|
|
2372
|
+
|
|
2373
|
+
project_root = Path(project_path).resolve()
|
|
2374
|
+
rows = []
|
|
2375
|
+
for consideration in sorted(load_cached_considerations(project_root), key=lambda item: item.id):
|
|
2376
|
+
row = consideration_to_dict(consideration)
|
|
2377
|
+
row["status"] = consideration_status(consideration, project_root)
|
|
2378
|
+
rows.append(row)
|
|
2379
|
+
|
|
2380
|
+
if output_format == "json":
|
|
2381
|
+
click.echo(json.dumps(rows, ensure_ascii=False, indent=2))
|
|
2382
|
+
return
|
|
2383
|
+
|
|
2384
|
+
if not rows:
|
|
2385
|
+
click.echo("No considerations found")
|
|
2386
|
+
return
|
|
2387
|
+
for row in rows:
|
|
2388
|
+
description = str(row.get("description") or "")
|
|
2389
|
+
click.echo(f"{row['id']}\t{row['status']}\t{description}")
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
|
|
2393
|
+
try:
|
|
2394
|
+
return load_project_config(project_root)
|
|
2395
|
+
except (FileNotFoundError, ValueError):
|
|
2396
|
+
return {}
|
|
2397
|
+
|
|
2398
|
+
|
|
2286
2399
|
@main.group()
|
|
2287
2400
|
def dag():
|
|
2288
2401
|
"""DAG Completeness Gate commands."""
|
|
@@ -2429,7 +2542,7 @@ def dag_journeys(project_path: str, output_format: str):
|
|
|
2429
2542
|
click.echo(f"Error: {exc}")
|
|
2430
2543
|
raise SystemExit(1)
|
|
2431
2544
|
|
|
2432
|
-
journeys = _collect_dag_journeys(built_dag)
|
|
2545
|
+
journeys = _collect_dag_journeys(built_dag, project_root, _load_optional_project_config(project_root))
|
|
2433
2546
|
if output_format == "json":
|
|
2434
2547
|
click.echo(json.dumps(journeys, ensure_ascii=False, indent=2))
|
|
2435
2548
|
return
|
|
@@ -2447,12 +2560,120 @@ def dag_journeys(project_path: str, output_format: str):
|
|
|
2447
2560
|
click.echo(f" {journey['name']} [{journey['criticality']}] requires: {requires}")
|
|
2448
2561
|
|
|
2449
2562
|
|
|
2450
|
-
|
|
2563
|
+
@dag.command("run-journey")
|
|
2564
|
+
@click.argument("journey_name")
|
|
2565
|
+
@click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
|
|
2566
|
+
@click.option(
|
|
2567
|
+
"--config-section",
|
|
2568
|
+
default="cdp_browser",
|
|
2569
|
+
show_default=True,
|
|
2570
|
+
help="verification.templates section used for browser config",
|
|
2571
|
+
)
|
|
2572
|
+
def dag_run_journey(journey_name: str, project_path: str, config_section: str):
|
|
2573
|
+
"""Run one declared user_journey with the CDP browser template."""
|
|
2574
|
+
from codd.dag.builder import build_dag
|
|
2575
|
+
from codd.deployment.providers.verification.cdp_browser import CdpBrowser
|
|
2576
|
+
|
|
2577
|
+
project_root = Path(project_path).resolve()
|
|
2578
|
+
try:
|
|
2579
|
+
config = load_project_config(project_root)
|
|
2580
|
+
template_config = _journey_template_config(config, config_section)
|
|
2581
|
+
built_dag = build_dag(project_root)
|
|
2582
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2583
|
+
click.echo(f"Error: {exc}")
|
|
2584
|
+
raise SystemExit(2)
|
|
2585
|
+
|
|
2586
|
+
journey_record = _find_dag_journey(built_dag, journey_name, project_root, config)
|
|
2587
|
+
if journey_record is None:
|
|
2588
|
+
click.echo(f"Error: user_journey not found: {journey_name}")
|
|
2589
|
+
raise SystemExit(2)
|
|
2590
|
+
|
|
2591
|
+
command = json.dumps(
|
|
2592
|
+
_journey_execution_plan(project_root, journey_record, template_config),
|
|
2593
|
+
sort_keys=True,
|
|
2594
|
+
)
|
|
2595
|
+
result = CdpBrowser(config=template_config).execute(command)
|
|
2596
|
+
if result.output:
|
|
2597
|
+
click.echo(result.output)
|
|
2598
|
+
raise SystemExit(0 if result.passed else 1)
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def _journey_template_config(config: dict[str, Any], config_section: str) -> dict[str, Any]:
|
|
2602
|
+
verification = config.get("verification")
|
|
2603
|
+
templates = verification.get("templates") if isinstance(verification, dict) else None
|
|
2604
|
+
if not isinstance(templates, dict) or config_section not in templates:
|
|
2605
|
+
raise ValueError(f"verification.templates.{config_section} config not found")
|
|
2606
|
+
|
|
2607
|
+
section = templates[config_section]
|
|
2608
|
+
if not isinstance(section, dict):
|
|
2609
|
+
raise ValueError(f"verification.templates.{config_section} must be a mapping")
|
|
2610
|
+
return dict(section)
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
def _find_dag_journey(
|
|
2614
|
+
dag: Any,
|
|
2615
|
+
journey_name: str,
|
|
2616
|
+
project_root: Path | None = None,
|
|
2617
|
+
settings: dict[str, Any] | None = None,
|
|
2618
|
+
) -> dict[str, Any] | None:
|
|
2619
|
+
for node in sorted(dag.nodes.values(), key=lambda item: item.id):
|
|
2620
|
+
if node.kind != "design_doc":
|
|
2621
|
+
continue
|
|
2622
|
+
for journey in _node_user_journeys(node, project_root, settings):
|
|
2623
|
+
if str(journey.get("name") or "") == journey_name:
|
|
2624
|
+
return {
|
|
2625
|
+
"design_doc": node.path or node.id,
|
|
2626
|
+
"journey": dict(journey),
|
|
2627
|
+
}
|
|
2628
|
+
return None
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
def _journey_execution_plan(
|
|
2632
|
+
project_root: Path,
|
|
2633
|
+
journey_record: dict[str, Any],
|
|
2634
|
+
template_config: dict[str, Any],
|
|
2635
|
+
) -> dict[str, Any]:
|
|
2636
|
+
journey = dict(journey_record["journey"])
|
|
2637
|
+
journey_name = str(journey.get("name") or "")
|
|
2638
|
+
steps = journey.get("steps")
|
|
2639
|
+
return {
|
|
2640
|
+
"template": "cdp_browser",
|
|
2641
|
+
"test_kind": "e2e",
|
|
2642
|
+
"target": _journey_target(journey),
|
|
2643
|
+
"identifier": f"journey:{journey_name}",
|
|
2644
|
+
"journey": journey_name,
|
|
2645
|
+
"steps": steps if isinstance(steps, list) else [],
|
|
2646
|
+
"project_root": str(project_root),
|
|
2647
|
+
"design_doc": journey_record["design_doc"],
|
|
2648
|
+
"config": template_config,
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
def _journey_target(journey: dict[str, Any]) -> str:
|
|
2653
|
+
steps = journey.get("steps")
|
|
2654
|
+
if isinstance(steps, list):
|
|
2655
|
+
for step in steps:
|
|
2656
|
+
if not isinstance(step, dict):
|
|
2657
|
+
continue
|
|
2658
|
+
if step.get("action") == "navigate":
|
|
2659
|
+
target = step.get("target") or step.get("url")
|
|
2660
|
+
if target:
|
|
2661
|
+
return str(target)
|
|
2662
|
+
|
|
2663
|
+
target = journey.get("target") or journey.get("url")
|
|
2664
|
+
return str(target or "")
|
|
2665
|
+
|
|
2666
|
+
|
|
2667
|
+
def _collect_dag_journeys(
|
|
2668
|
+
dag: Any,
|
|
2669
|
+
project_root: Path | None = None,
|
|
2670
|
+
settings: dict[str, Any] | None = None,
|
|
2671
|
+
) -> list[dict[str, Any]]:
|
|
2451
2672
|
journeys: list[dict[str, Any]] = []
|
|
2452
2673
|
for node in sorted(dag.nodes.values(), key=lambda item: item.id):
|
|
2453
2674
|
if node.kind != "design_doc":
|
|
2454
2675
|
continue
|
|
2455
|
-
for journey in _node_user_journeys(node):
|
|
2676
|
+
for journey in _node_user_journeys(node, project_root, settings):
|
|
2456
2677
|
journeys.append(
|
|
2457
2678
|
{
|
|
2458
2679
|
"design_doc": node.path or node.id,
|
|
@@ -2464,7 +2685,16 @@ def _collect_dag_journeys(dag: Any) -> list[dict[str, Any]]:
|
|
|
2464
2685
|
return journeys
|
|
2465
2686
|
|
|
2466
2687
|
|
|
2467
|
-
def _node_user_journeys(
|
|
2688
|
+
def _node_user_journeys(
|
|
2689
|
+
node: Any,
|
|
2690
|
+
project_root: Path | None = None,
|
|
2691
|
+
settings: dict[str, Any] | None = None,
|
|
2692
|
+
) -> list[dict[str, Any]]:
|
|
2693
|
+
if project_root is not None:
|
|
2694
|
+
from codd.dag.checks.user_journey_coherence import UserJourneyCoherenceCheck
|
|
2695
|
+
|
|
2696
|
+
return UserJourneyCoherenceCheck(project_root=project_root, settings=settings or {})._journey_entries(node)
|
|
2697
|
+
|
|
2468
2698
|
attributes = getattr(node, "attributes", {}) or {}
|
|
2469
2699
|
value = attributes.get("user_journeys")
|
|
2470
2700
|
if not isinstance(value, list):
|
|
@@ -29,9 +29,9 @@ from codd.deployment.extractor import (
|
|
|
29
29
|
|
|
30
30
|
LOGGER = logging.getLogger(__name__)
|
|
31
31
|
DEFAULTS_DIR = Path(__file__).parent / "defaults"
|
|
32
|
-
DEFAULT_PROJECT_TYPE = "
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
DEFAULT_PROJECT_TYPE = "generic"
|
|
33
|
+
LEGACY_IMPLEMENTATION_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java")
|
|
34
|
+
LEGACY_TEST_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".bats")
|
|
35
35
|
PLAN_HEADER_RE = re.compile(r"^#{2,6}\s+([A-Za-z0-9]+(?:[-_.][A-Za-z0-9]+)*)(?:\s+(.+))?$", re.MULTILINE)
|
|
36
36
|
OUTPUTS_RE = re.compile(r"(?im)^outputs?[ \t]*:[ \t]*(.*)$")
|
|
37
37
|
PY_IMPORT_RE = re.compile(r"(?m)^\s*(?:from\s+([A-Za-z_][\w.]*)(?:\s+import\s+)|import\s+([A-Za-z_][\w.]*))")
|
|
@@ -88,17 +88,21 @@ def build_dag(project_root: Path, settings: dict[str, Any] | None = None) -> DAG
|
|
|
88
88
|
def load_dag_settings(project_root: Path, settings: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
89
89
|
"""Load project-type defaults and apply ``codd.yaml dag:`` overrides."""
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
root = Path(project_root).resolve()
|
|
92
|
+
project_config = _load_project_config_or_empty(root)
|
|
92
93
|
|
|
93
94
|
requested_settings = settings or {}
|
|
94
|
-
project_type = _project_type(requested_settings) or _project_type(project_config) or
|
|
95
|
+
project_type = _project_type(requested_settings) or _project_type(project_config) or _detect_project_type(root)
|
|
95
96
|
merged = _read_default_settings(project_type)
|
|
96
97
|
merged = _deep_merge(merged, _dag_overrides(project_config))
|
|
97
98
|
merged = _deep_merge(merged, _dag_overrides(requested_settings))
|
|
99
|
+
merged["project_type"] = project_type
|
|
100
|
+
implementation_suffixes, test_suffixes = _load_suffix_config(root, merged)
|
|
101
|
+
merged["implementation_suffixes"] = implementation_suffixes
|
|
102
|
+
merged["test_suffixes"] = test_suffixes
|
|
98
103
|
_apply_scan_patterns(merged, project_config)
|
|
99
104
|
_apply_scan_patterns(merged, requested_settings)
|
|
100
105
|
merged["coherence"] = _coherence_settings(project_config, requested_settings)
|
|
101
|
-
merged["project_type"] = project_type
|
|
102
106
|
merged.setdefault("design_doc_patterns", [])
|
|
103
107
|
merged.setdefault("impl_file_patterns", [])
|
|
104
108
|
merged.setdefault("test_file_patterns", [])
|
|
@@ -214,10 +218,11 @@ def _add_design_docs(dag: DAG, project_root: Path, settings: dict[str, Any]) ->
|
|
|
214
218
|
def _add_impl_files(dag: DAG, project_root: Path, settings: dict[str, Any]) -> dict[str, Path]:
|
|
215
219
|
impl_nodes: dict[str, Path] = {}
|
|
216
220
|
capability_patterns = _capability_patterns(settings)
|
|
221
|
+
implementation_suffixes = _suffix_tuple(settings.get("implementation_suffixes")) or LEGACY_IMPLEMENTATION_SUFFIXES
|
|
217
222
|
for file_path in _glob_project_paths(project_root, settings.get("impl_file_patterns", [])):
|
|
218
223
|
if (
|
|
219
224
|
not file_path.is_file()
|
|
220
|
-
or file_path.suffix not in
|
|
225
|
+
or file_path.suffix not in implementation_suffixes
|
|
221
226
|
or _is_test_file(file_path, project_root)
|
|
222
227
|
):
|
|
223
228
|
continue
|
|
@@ -241,8 +246,9 @@ def _add_impl_files(dag: DAG, project_root: Path, settings: dict[str, Any]) -> d
|
|
|
241
246
|
|
|
242
247
|
def _add_test_files(dag: DAG, project_root: Path, settings: dict[str, Any]) -> dict[str, Path]:
|
|
243
248
|
test_nodes: dict[str, Path] = {}
|
|
249
|
+
test_suffixes = _suffix_tuple(settings.get("test_suffixes")) or LEGACY_TEST_SUFFIXES
|
|
244
250
|
for file_path in _glob_project_paths(project_root, settings.get("test_file_patterns", [])):
|
|
245
|
-
if not file_path.is_file() or file_path.suffix not in
|
|
251
|
+
if not file_path.is_file() or file_path.suffix not in test_suffixes or not _is_test_file(file_path, project_root):
|
|
246
252
|
continue
|
|
247
253
|
node_id = _relative_id(file_path, project_root)
|
|
248
254
|
test_nodes[node_id] = file_path.resolve()
|
|
@@ -445,8 +451,9 @@ def _add_deployment_graph(
|
|
|
445
451
|
project_root,
|
|
446
452
|
deployment_docs,
|
|
447
453
|
[{"id": node_id, **metadata} for node_id, metadata in design_docs.items()],
|
|
454
|
+
project_config,
|
|
448
455
|
)
|
|
449
|
-
verification_tests = extract_verification_tests(project_root)
|
|
456
|
+
verification_tests = extract_verification_tests(project_root, project_config, design_docs)
|
|
450
457
|
|
|
451
458
|
for deployment_doc in deployment_docs:
|
|
452
459
|
_add_node_once(
|
|
@@ -768,7 +775,18 @@ def _impl_convention_tokens(node_id: str) -> set[str]:
|
|
|
768
775
|
|
|
769
776
|
def _convention_path_candidates(test_path: Path, project_root: Path, convention_key: str) -> list[Path]:
|
|
770
777
|
candidates: list[Path] = []
|
|
771
|
-
|
|
778
|
+
suffix_groups = {
|
|
779
|
+
".py": [".py"],
|
|
780
|
+
".rs": [".rs"],
|
|
781
|
+
".rb": [".rb"],
|
|
782
|
+
".cs": [".cs"],
|
|
783
|
+
".kt": [".kt", ".kts"],
|
|
784
|
+
".swift": [".swift"],
|
|
785
|
+
".exs": [".ex", ".exs"],
|
|
786
|
+
".scala": [".scala"],
|
|
787
|
+
".cpp": [".cpp", ".c", ".h", ".hpp"],
|
|
788
|
+
}
|
|
789
|
+
suffixes = suffix_groups.get(test_path.suffix, [".ts", ".tsx", ".js", ".jsx"])
|
|
772
790
|
|
|
773
791
|
if any(marker in test_path.name for marker in (".test.", ".spec.")):
|
|
774
792
|
candidates.append((test_path.parent / convention_key).resolve())
|
|
@@ -859,6 +877,128 @@ def _read_default_settings(project_type: str) -> dict[str, Any]:
|
|
|
859
877
|
return payload if isinstance(payload, dict) else {}
|
|
860
878
|
|
|
861
879
|
|
|
880
|
+
def _detect_project_type(project_root: Path) -> str:
|
|
881
|
+
root = Path(project_root)
|
|
882
|
+
if (root / "Cargo.toml").is_file():
|
|
883
|
+
return "rust"
|
|
884
|
+
if (root / "Gemfile").is_file():
|
|
885
|
+
return "ruby"
|
|
886
|
+
if (root / "package.json").is_file():
|
|
887
|
+
return "web"
|
|
888
|
+
if (root / "go.mod").is_file():
|
|
889
|
+
return "go"
|
|
890
|
+
if any(root.glob("*.csproj")) or any(root.glob("*.sln")):
|
|
891
|
+
return "csharp"
|
|
892
|
+
if (root / "CMakeLists.txt").is_file() or (
|
|
893
|
+
(root / "Makefile").is_file() and _has_any_file(root, ("*.c", "*.cpp"))
|
|
894
|
+
):
|
|
895
|
+
return "cpp_embedded"
|
|
896
|
+
if any(root.glob("*.gradle.kts")):
|
|
897
|
+
return "kotlin"
|
|
898
|
+
if (root / "pom.xml").is_file():
|
|
899
|
+
return "java"
|
|
900
|
+
if (root / "build.gradle").is_file() or any(root.glob("*.gradle")):
|
|
901
|
+
if _has_any_file(root, ("*.kt", "*.kts")):
|
|
902
|
+
return "kotlin"
|
|
903
|
+
return "java"
|
|
904
|
+
if (root / "mix.exs").is_file():
|
|
905
|
+
return "elixir"
|
|
906
|
+
if (root / "build.sbt").is_file():
|
|
907
|
+
return "scala"
|
|
908
|
+
if _has_any_file(root, ("*.swift",)):
|
|
909
|
+
return "swift"
|
|
910
|
+
return "generic"
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _has_any_file(project_root: Path, patterns: tuple[str, ...]) -> bool:
|
|
914
|
+
for pattern in patterns:
|
|
915
|
+
if any(path.is_file() for path in project_root.rglob(pattern)):
|
|
916
|
+
return True
|
|
917
|
+
return False
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _load_suffix_config(project_root: Path, codd_yaml: dict[str, Any]) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
921
|
+
implementation_suffixes = _suffixes_from_config(codd_yaml, "implementation_suffixes")
|
|
922
|
+
test_suffixes = _suffixes_from_config(codd_yaml, "test_suffixes")
|
|
923
|
+
project_type = _project_type(codd_yaml) or _detect_project_type(project_root)
|
|
924
|
+
defaults = _read_suffix_default_mapping(project_type)
|
|
925
|
+
|
|
926
|
+
if implementation_suffixes is None:
|
|
927
|
+
implementation_suffixes = _suffixes_from_config(defaults, "implementation_suffixes")
|
|
928
|
+
if implementation_suffixes is None:
|
|
929
|
+
LOGGER.warning(
|
|
930
|
+
"DAG suffix defaults for project_type=%s missing implementation_suffixes; using legacy fallback",
|
|
931
|
+
project_type,
|
|
932
|
+
)
|
|
933
|
+
implementation_suffixes = LEGACY_IMPLEMENTATION_SUFFIXES
|
|
934
|
+
if test_suffixes is None:
|
|
935
|
+
test_suffixes = _suffixes_from_config(defaults, "test_suffixes")
|
|
936
|
+
if test_suffixes is None:
|
|
937
|
+
LOGGER.warning(
|
|
938
|
+
"DAG suffix defaults for project_type=%s missing test_suffixes; using legacy fallback",
|
|
939
|
+
project_type,
|
|
940
|
+
)
|
|
941
|
+
test_suffixes = LEGACY_TEST_SUFFIXES
|
|
942
|
+
|
|
943
|
+
implementation_suffixes = _extend_suffixes(
|
|
944
|
+
implementation_suffixes,
|
|
945
|
+
_suffixes_from_config(codd_yaml, "implementation_suffixes_extend"),
|
|
946
|
+
)
|
|
947
|
+
test_suffixes = _extend_suffixes(
|
|
948
|
+
test_suffixes,
|
|
949
|
+
_suffixes_from_config(codd_yaml, "test_suffixes_extend"),
|
|
950
|
+
)
|
|
951
|
+
return implementation_suffixes, test_suffixes
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _read_suffix_default_mapping(project_type: str) -> dict[str, Any]:
|
|
955
|
+
for suffix_type in (project_type, DEFAULT_PROJECT_TYPE):
|
|
956
|
+
path = DEFAULTS_DIR / f"{suffix_type}.yaml"
|
|
957
|
+
if not path.is_file():
|
|
958
|
+
continue
|
|
959
|
+
try:
|
|
960
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
961
|
+
except yaml.YAMLError as exc:
|
|
962
|
+
LOGGER.warning("Could not load DAG suffix defaults from %s: %s", path, exc)
|
|
963
|
+
return {}
|
|
964
|
+
if isinstance(payload, dict):
|
|
965
|
+
return payload
|
|
966
|
+
LOGGER.warning("DAG suffix defaults %s must contain a YAML mapping; using legacy fallback", path)
|
|
967
|
+
return {}
|
|
968
|
+
LOGGER.warning("DAG suffix defaults for project_type=%s not found; using legacy fallback", project_type)
|
|
969
|
+
return {}
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _suffixes_from_config(config: dict[str, Any], key: str) -> tuple[str, ...] | None:
|
|
973
|
+
value = config.get(key)
|
|
974
|
+
if value is None and isinstance(config.get("dag"), dict):
|
|
975
|
+
value = config["dag"].get(key)
|
|
976
|
+
return _suffix_tuple(value)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _suffix_tuple(value: Any) -> tuple[str, ...] | None:
|
|
980
|
+
if value is None:
|
|
981
|
+
return None
|
|
982
|
+
suffixes: list[str] = []
|
|
983
|
+
for item in _as_list(value):
|
|
984
|
+
if not isinstance(item, str) or not item.strip():
|
|
985
|
+
continue
|
|
986
|
+
suffix = item.strip()
|
|
987
|
+
if not suffix.startswith("."):
|
|
988
|
+
suffix = f".{suffix}"
|
|
989
|
+
if suffix not in suffixes:
|
|
990
|
+
suffixes.append(suffix)
|
|
991
|
+
return tuple(suffixes) if suffixes else None
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _extend_suffixes(base: tuple[str, ...], extensions: tuple[str, ...] | None) -> tuple[str, ...]:
|
|
995
|
+
suffixes = list(base)
|
|
996
|
+
for suffix in extensions or ():
|
|
997
|
+
if suffix not in suffixes:
|
|
998
|
+
suffixes.append(suffix)
|
|
999
|
+
return tuple(suffixes)
|
|
1000
|
+
|
|
1001
|
+
|
|
862
1002
|
def _dag_overrides(config: dict[str, Any]) -> dict[str, Any]:
|
|
863
1003
|
overrides: dict[str, Any] = {}
|
|
864
1004
|
dag_section = config.get("dag", {})
|
|
@@ -869,6 +1009,10 @@ def _dag_overrides(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
869
1009
|
"design_doc_patterns",
|
|
870
1010
|
"impl_file_patterns",
|
|
871
1011
|
"test_file_patterns",
|
|
1012
|
+
"implementation_suffixes",
|
|
1013
|
+
"implementation_suffixes_extend",
|
|
1014
|
+
"test_suffixes",
|
|
1015
|
+
"test_suffixes_extend",
|
|
872
1016
|
"plan_task_file",
|
|
873
1017
|
"lexicon_file",
|
|
874
1018
|
"import_aliases",
|
|
@@ -922,13 +1066,16 @@ def _apply_scan_patterns(settings: dict[str, Any], config: dict[str, Any]) -> No
|
|
|
922
1066
|
_extend_unique(
|
|
923
1067
|
settings,
|
|
924
1068
|
"impl_file_patterns",
|
|
925
|
-
_file_patterns_for_dirs(
|
|
1069
|
+
_file_patterns_for_dirs(
|
|
1070
|
+
source_dirs,
|
|
1071
|
+
_suffix_tuple(settings.get("implementation_suffixes")) or LEGACY_IMPLEMENTATION_SUFFIXES,
|
|
1072
|
+
),
|
|
926
1073
|
)
|
|
927
1074
|
if test_dirs:
|
|
928
1075
|
_extend_unique(
|
|
929
1076
|
settings,
|
|
930
1077
|
"test_file_patterns",
|
|
931
|
-
_file_patterns_for_dirs(test_dirs,
|
|
1078
|
+
_file_patterns_for_dirs(test_dirs, _suffix_tuple(settings.get("test_suffixes")) or LEGACY_TEST_SUFFIXES),
|
|
932
1079
|
)
|
|
933
1080
|
if doc_dirs:
|
|
934
1081
|
_extend_unique(settings, "design_doc_patterns", _file_patterns_for_dirs(doc_dirs, (".md",)))
|
|
@@ -1034,11 +1181,25 @@ def _language_for_path(path: Path) -> str:
|
|
|
1034
1181
|
".jsx": "javascript",
|
|
1035
1182
|
".go": "go",
|
|
1036
1183
|
".java": "java",
|
|
1184
|
+
".rs": "rust",
|
|
1185
|
+
".rb": "ruby",
|
|
1186
|
+
".cs": "csharp",
|
|
1187
|
+
".c": "c",
|
|
1188
|
+
".cpp": "cpp",
|
|
1189
|
+
".h": "cpp",
|
|
1190
|
+
".hpp": "cpp",
|
|
1191
|
+
".kt": "kotlin",
|
|
1192
|
+
".kts": "kotlin",
|
|
1193
|
+
".swift": "swift",
|
|
1194
|
+
".ex": "elixir",
|
|
1195
|
+
".exs": "elixir",
|
|
1196
|
+
".scala": "scala",
|
|
1037
1197
|
}.get(path.suffix, "unknown")
|
|
1038
1198
|
|
|
1039
1199
|
|
|
1040
1200
|
def _looks_like_project_path(value: str) -> bool:
|
|
1041
|
-
|
|
1201
|
+
generic_suffixes, _ = _load_suffix_config(Path.cwd(), {"project_type": DEFAULT_PROJECT_TYPE})
|
|
1202
|
+
return "/" in value and Path(value).suffix in generic_suffixes
|
|
1042
1203
|
|
|
1043
1204
|
|
|
1044
1205
|
def _mermaid_id(node_id: str) -> str:
|
|
@@ -122,7 +122,9 @@ class TaskCompletionCheck:
|
|
|
122
122
|
def _impl_file_exists(self, impl_node: Any, fallback_path: str, project_root: Path | None) -> bool:
|
|
123
123
|
if project_root is None:
|
|
124
124
|
return True
|
|
125
|
-
|
|
125
|
+
attributes = getattr(impl_node, "attributes", {}) or {}
|
|
126
|
+
attribute_path = attributes.get("path") if isinstance(attributes, dict) else None
|
|
127
|
+
node_path = getattr(impl_node, "path", None) or attribute_path or fallback_path
|
|
126
128
|
candidate = Path(node_path)
|
|
127
129
|
if not candidate.is_absolute():
|
|
128
130
|
candidate = project_root / candidate
|