codd-dev 1.20.0__tar.gz → 1.22.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.20.0 → codd_dev-1.22.0}/PKG-INFO +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/__init__.py +1 -1
- codd_dev-1.22.0/codd/__main__.py +7 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/ask_user_question_adapter.py +19 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/cli.py +303 -25
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/coherence_engine.py +12 -7
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/contracts.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/coverage_metrics.py +114 -4
- codd_dev-1.22.0/codd/dag/__init__.py +112 -0
- codd_dev-1.22.0/codd/dag/builder.py +569 -0
- codd_dev-1.22.0/codd/dag/checks/__init__.py +39 -0
- codd_dev-1.22.0/codd/dag/checks/depends_on_consistency.py +320 -0
- codd_dev-1.22.0/codd/dag/checks/edge_validity.py +71 -0
- codd_dev-1.22.0/codd/dag/checks/node_completeness.py +63 -0
- codd_dev-1.22.0/codd/dag/checks/task_completion.py +162 -0
- codd_dev-1.22.0/codd/dag/checks/transitive_closure.py +64 -0
- codd_dev-1.22.0/codd/dag/defaults/cli.yaml +6 -0
- codd_dev-1.22.0/codd/dag/defaults/iot.yaml +6 -0
- codd_dev-1.22.0/codd/dag/defaults/mobile.yaml +6 -0
- codd_dev-1.22.0/codd/dag/defaults/web.yaml +14 -0
- codd_dev-1.22.0/codd/dag/extractor.py +76 -0
- codd_dev-1.22.0/codd/dag/runner.py +74 -0
- codd_dev-1.22.0/codd/deployer.py +711 -0
- codd_dev-1.22.0/codd/drift_linkers/__init__.py +46 -0
- codd_dev-1.22.0/codd/drift_linkers/api.py +484 -0
- codd_dev-1.22.0/codd/drift_linkers/defaults/cli.yaml +1 -0
- codd_dev-1.22.0/codd/drift_linkers/defaults/iot.yaml +1 -0
- codd_dev-1.22.0/codd/drift_linkers/defaults/mobile.yaml +2 -0
- codd_dev-1.22.0/codd/drift_linkers/defaults/web.yaml +8 -0
- codd_dev-1.22.0/codd/drift_linkers/schema.py +262 -0
- codd_dev-1.22.0/codd/drift_linkers/screen_flow.py +171 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/env_refs.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/extract_ai.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/extractor.py +10 -10
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixer.py +6 -6
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/generator.py +4 -4
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/graph.py +4 -4
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/hooks/__init__.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/measure.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/policy.py +1 -1
- codd_dev-1.22.0/codd/preflight/__init__.py +353 -0
- codd_dev-1.22.0/codd/preflight/defaults/cli.yaml +9 -0
- codd_dev-1.22.0/codd/preflight/defaults/iot.yaml +7 -0
- codd_dev-1.22.0/codd/preflight/defaults/mobile.yaml +8 -0
- codd_dev-1.22.0/codd/preflight/defaults/web.yaml +14 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/propagate.py +3 -3
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/propagator.py +4 -4
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/repair_slice.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/required_artifacts_deriver.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/requirement_completeness_auditor.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/scanner.py +6 -6
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/schema_refs.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/traceability.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/validator.py +3 -3
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/wiring.py +1 -1
- {codd_dev-1.20.0 → codd_dev-1.22.0}/pyproject.toml +1 -1
- codd_dev-1.20.0/codd/deployer.py +0 -224
- {codd_dev-1.20.0 → codd_dev-1.22.0}/.gitignore +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/LICENSE +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/README.md +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/_git_helper.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/assembler.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/bridge.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/clustering.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/coherence_adapters.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/config.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/coverage_auditor.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/defaults.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/deploy_targets/__init__.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/deploy_targets/app_service.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/deploy_targets/base.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/deploy_targets/docker_compose.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/design_md.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/drift.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/e2e_extractor.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/e2e_generator.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/e2e_runner.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixup_drift.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixup_drift_strategies/__init__.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/hitl_session.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/hooks/pre-commit +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/implementer.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/inheritance.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/knowledge_fetcher.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/lexicon.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/mcp_server.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/parsing.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/planner.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/registry.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/require.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/require_plugins.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/require_propagate.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/required_artifacts/defaults/web.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/restore.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/routes_extractor.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/screen_flow_validator.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/screen_transition_extractor.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/screen_transitions/defaults.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/synth.py +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/lexicon_schema.yaml +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-1.20.0 → codd_dev-1.22.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.22.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
|
|
@@ -13,6 +13,7 @@ from codd.lexicon import AskItem
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
DEFAULT_CHANNELS = ["askuserquestion", "ntfy", "lexicon"]
|
|
16
|
+
SEVERITY_ORDER = ["critical", "high", "medium", "low"]
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def send_ask_items(
|
|
@@ -20,6 +21,7 @@ def send_ask_items(
|
|
|
20
21
|
channels: list[str] = DEFAULT_CHANNELS,
|
|
21
22
|
ntfy_topic: str = "",
|
|
22
23
|
lexicon_path: str | Path | None = None,
|
|
24
|
+
ntfy_severity_threshold: str = "critical",
|
|
23
25
|
) -> None:
|
|
24
26
|
"""Send ASK items through Claude, ntfy, and lexicon channels when available."""
|
|
25
27
|
normalized_channels = {channel.lower() for channel in channels}
|
|
@@ -30,6 +32,8 @@ def send_ask_items(
|
|
|
30
32
|
|
|
31
33
|
if "ntfy" in normalized_channels and ntfy_topic:
|
|
32
34
|
for item in ask_items:
|
|
35
|
+
if not _severity_at_or_above(_ask_item_severity(item), ntfy_severity_threshold):
|
|
36
|
+
continue
|
|
33
37
|
_post_ntfy(ntfy_topic, format_ask_for_ntfy(item))
|
|
34
38
|
|
|
35
39
|
if "lexicon" in normalized_channels and lexicon_path is not None:
|
|
@@ -65,6 +69,21 @@ def parse_user_answer(raw: str, item: AskItem) -> str:
|
|
|
65
69
|
return answer
|
|
66
70
|
|
|
67
71
|
|
|
72
|
+
def _severity_at_or_above(item_severity: str, threshold: str) -> bool:
|
|
73
|
+
"""Return True when item_severity is at least as severe as threshold."""
|
|
74
|
+
try:
|
|
75
|
+
return SEVERITY_ORDER.index(item_severity) <= SEVERITY_ORDER.index(threshold)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ask_item_severity(item: AskItem) -> str:
|
|
81
|
+
severity = getattr(item, "severity", "")
|
|
82
|
+
if isinstance(severity, str) and severity:
|
|
83
|
+
return severity.lower()
|
|
84
|
+
return "critical" if item.blocking else "high"
|
|
85
|
+
|
|
86
|
+
|
|
68
87
|
def _send_ask_user_question(item: AskItem) -> bool:
|
|
69
88
|
"""Best-effort hook for Claude Code AskUserQuestion integrations."""
|
|
70
89
|
try:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
"""CoDD CLI — codd init / scan / impact / require / plan."""
|
|
2
|
-
|
|
1
|
+
"""CoDD CLI — codd init / scan / impact / require / plan."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, is_dataclass
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
@@ -189,14 +190,86 @@ def _ensure_bootstrap_codd_yaml(
|
|
|
189
190
|
|
|
190
191
|
@click.group()
|
|
191
192
|
@click.version_option(package_name="codd-dev")
|
|
192
|
-
def main():
|
|
193
|
-
"""CoDD: Coherence-Driven Development."""
|
|
194
|
-
pass
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
@main.command()
|
|
198
|
-
@click.
|
|
199
|
-
@click.option("--
|
|
193
|
+
def main():
|
|
194
|
+
"""CoDD: Coherence-Driven Development."""
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@main.command("preflight")
|
|
199
|
+
@click.argument("task_yaml", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
200
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
201
|
+
@click.option("--strict", is_flag=True, help="Treat high severity as halt-worthy")
|
|
202
|
+
@click.option("--ntfy-topic", default="", help="ntfy topic for critical alerts")
|
|
203
|
+
@click.option(
|
|
204
|
+
"--ntfy-severity-threshold",
|
|
205
|
+
default=None,
|
|
206
|
+
help="Minimum severity sent to ntfy (default: codd.yaml preflight.ntfy_severity_threshold or critical)",
|
|
207
|
+
)
|
|
208
|
+
def preflight(task_yaml: Path, project_path: str, strict: bool, ntfy_topic: str, ntfy_severity_threshold: str | None):
|
|
209
|
+
"""Run preflight checks on a task YAML before autonomous execution."""
|
|
210
|
+
_run_preflight_command(task_yaml, project_path, strict, ntfy_topic, ntfy_severity_threshold)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@main.command("gungi")
|
|
214
|
+
@click.argument("task_yaml", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
215
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
216
|
+
@click.option("--strict", is_flag=True, help="Treat high severity as halt-worthy")
|
|
217
|
+
@click.option("--ntfy-topic", default="", help="ntfy topic for critical alerts")
|
|
218
|
+
@click.option(
|
|
219
|
+
"--ntfy-severity-threshold",
|
|
220
|
+
default=None,
|
|
221
|
+
help="Minimum severity sent to ntfy (default: codd.yaml preflight.ntfy_severity_threshold or critical)",
|
|
222
|
+
)
|
|
223
|
+
def gungi(task_yaml: Path, project_path: str, strict: bool, ntfy_topic: str, ntfy_severity_threshold: str | None):
|
|
224
|
+
"""Alias for preflight."""
|
|
225
|
+
_run_preflight_command(task_yaml, project_path, strict, ntfy_topic, ntfy_severity_threshold)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _run_preflight_command(
|
|
229
|
+
task_yaml: Path,
|
|
230
|
+
project_path: str,
|
|
231
|
+
strict: bool,
|
|
232
|
+
ntfy_topic: str,
|
|
233
|
+
ntfy_severity_threshold: str | None,
|
|
234
|
+
) -> None:
|
|
235
|
+
from codd.ask_user_question_adapter import _post_ntfy, _severity_at_or_above
|
|
236
|
+
from codd.preflight import PreflightAuditor
|
|
237
|
+
|
|
238
|
+
project_root = Path(project_path).resolve()
|
|
239
|
+
auditor = PreflightAuditor(project_root=project_root)
|
|
240
|
+
result = auditor.run(task_yaml)
|
|
241
|
+
strict_halt = strict and result.severity == "high"
|
|
242
|
+
if strict_halt:
|
|
243
|
+
result.halt_recommended = True
|
|
244
|
+
|
|
245
|
+
threshold = ntfy_severity_threshold or str(
|
|
246
|
+
auditor.preflight_config.get("ntfy_severity_threshold") or "critical"
|
|
247
|
+
)
|
|
248
|
+
if ntfy_topic and _severity_at_or_above(result.severity, threshold):
|
|
249
|
+
result.ntfy_sent = _post_ntfy(ntfy_topic, _format_preflight_ntfy(result))
|
|
250
|
+
|
|
251
|
+
for check in result.checks:
|
|
252
|
+
click.echo(f"[{check.status}] {check.name}: {check.message}")
|
|
253
|
+
for detail in check.details:
|
|
254
|
+
click.echo(f" - {detail}")
|
|
255
|
+
click.echo(f"Overall severity: {result.severity}")
|
|
256
|
+
click.echo(f"ntfy_sent: {str(result.ntfy_sent).lower()}")
|
|
257
|
+
|
|
258
|
+
if result.halt_recommended:
|
|
259
|
+
reason = "strict high severity" if strict_halt else "critical issue found"
|
|
260
|
+
click.echo(f"HALT recommended: {reason}")
|
|
261
|
+
raise SystemExit(1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _format_preflight_ntfy(result: Any) -> str:
|
|
265
|
+
failed = [check.name for check in result.checks if check.status in {"FAIL", "WARN"}]
|
|
266
|
+
suffix = f" ({', '.join(failed)})" if failed else ""
|
|
267
|
+
return f"CoDD preflight: {result.task_id} severity={result.severity}{suffix}"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@main.command()
|
|
271
|
+
@click.option("--project-name", prompt="Project name", help="Name of the project")
|
|
272
|
+
@click.option("--language", prompt="Primary language", help="Primary language (python/typescript/javascript/go — full support; java — symbols only)")
|
|
200
273
|
@click.option("--dest", default=".", help="Destination directory (default: current dir)")
|
|
201
274
|
@click.option(
|
|
202
275
|
"--requirements",
|
|
@@ -239,7 +312,7 @@ def init(project_name: str, language: str, dest: str, requirements: str | None,
|
|
|
239
312
|
_render_template("gitignore.tmpl", codd_dir / ".gitignore", {})
|
|
240
313
|
|
|
241
314
|
# Version file
|
|
242
|
-
(dest_path / ".codd_version").write_text("0.2.0\n")
|
|
315
|
+
(dest_path / ".codd_version").write_text("0.2.0\n", encoding="utf-8")
|
|
243
316
|
|
|
244
317
|
# Import requirements if provided
|
|
245
318
|
if requirements:
|
|
@@ -1197,7 +1270,7 @@ def repair_slice_cmd(path, files, issue, issue_file, language, source_dirs, top_
|
|
|
1197
1270
|
|
|
1198
1271
|
issue_text = issue or ""
|
|
1199
1272
|
if issue_file and not issue_text:
|
|
1200
|
-
issue_text = Path(issue_file).read_text(errors="ignore")
|
|
1273
|
+
issue_text = Path(issue_file).read_text(encoding="utf-8", errors="ignore")
|
|
1201
1274
|
|
|
1202
1275
|
dirs = [d.strip() for d in source_dirs.split(",") if d.strip()] if source_dirs else None
|
|
1203
1276
|
|
|
@@ -1353,8 +1426,21 @@ def validate(lexicon: bool, design_tokens: bool, screen_flow: bool, edges: bool,
|
|
|
1353
1426
|
type=float,
|
|
1354
1427
|
help="Lexicon compliance threshold percentage.",
|
|
1355
1428
|
)
|
|
1429
|
+
@click.option(
|
|
1430
|
+
"--screen-flow-threshold",
|
|
1431
|
+
default=100.0,
|
|
1432
|
+
show_default=True,
|
|
1433
|
+
type=float,
|
|
1434
|
+
help="Screen-flow coverage threshold percentage.",
|
|
1435
|
+
)
|
|
1356
1436
|
@click.option("--json", "as_json", is_flag=True, help="Output machine-readable JSON.")
|
|
1357
|
-
def coverage(
|
|
1437
|
+
def coverage(
|
|
1438
|
+
path: str,
|
|
1439
|
+
e2e_threshold: float,
|
|
1440
|
+
lexicon_threshold: float,
|
|
1441
|
+
screen_flow_threshold: float,
|
|
1442
|
+
as_json: bool,
|
|
1443
|
+
):
|
|
1358
1444
|
"""Coverage metrics merge gate: E2E, design tokens, and lexicon."""
|
|
1359
1445
|
from codd.coverage_metrics import run_coverage
|
|
1360
1446
|
|
|
@@ -1364,6 +1450,7 @@ def coverage(path: str, e2e_threshold: float, lexicon_threshold: float, as_json:
|
|
|
1364
1450
|
e2e_threshold=e2e_threshold,
|
|
1365
1451
|
design_token_threshold=0.0,
|
|
1366
1452
|
lexicon_threshold=lexicon_threshold,
|
|
1453
|
+
screen_flow_threshold=screen_flow_threshold,
|
|
1367
1454
|
)
|
|
1368
1455
|
|
|
1369
1456
|
if as_json:
|
|
@@ -2079,7 +2166,7 @@ def measure(path: str, as_json: bool):
|
|
|
2079
2166
|
|
|
2080
2167
|
@main.command("mcp-server")
|
|
2081
2168
|
@click.option("--project", default=".", help="Project root directory")
|
|
2082
|
-
def mcp_server(project: str):
|
|
2169
|
+
def mcp_server(project: str):
|
|
2083
2170
|
"""Start MCP server for AI tool integration (stdio).
|
|
2084
2171
|
|
|
2085
2172
|
Exposes CoDD tools (validate, impact, policy, audit, scan) via the
|
|
@@ -2092,14 +2179,205 @@ def mcp_server(project: str):
|
|
|
2092
2179
|
from codd.mcp_server import run_stdio
|
|
2093
2180
|
|
|
2094
2181
|
project_root = Path(project).resolve()
|
|
2095
|
-
_require_codd_dir(project_root)
|
|
2096
|
-
run_stdio(project_root)
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
@main.group()
|
|
2100
|
-
def
|
|
2101
|
-
"""
|
|
2102
|
-
pass
|
|
2182
|
+
_require_codd_dir(project_root)
|
|
2183
|
+
run_stdio(project_root)
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
@main.group()
|
|
2187
|
+
def dag():
|
|
2188
|
+
"""DAG Completeness Gate commands."""
|
|
2189
|
+
pass
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
@dag.command("build")
|
|
2193
|
+
@click.option("--path", "project_path", default=".", help="Project root directory")
|
|
2194
|
+
@click.option(
|
|
2195
|
+
"--format",
|
|
2196
|
+
"output_format",
|
|
2197
|
+
default="json",
|
|
2198
|
+
type=click.Choice(["json", "mermaid"]),
|
|
2199
|
+
help="Output format",
|
|
2200
|
+
)
|
|
2201
|
+
@click.option("--cache", is_flag=True, help="Use cached DAG if output exists")
|
|
2202
|
+
@click.option("--output", default=None, help="Output file (default: .codd/dag.json or .codd/dag.mmd)")
|
|
2203
|
+
def dag_build(project_path: str, output_format: str, cache: bool, output: str | None):
|
|
2204
|
+
"""Build the project DAG and output it under .codd/."""
|
|
2205
|
+
from codd.dag.builder import (
|
|
2206
|
+
build_dag,
|
|
2207
|
+
default_dag_json_path,
|
|
2208
|
+
default_dag_mermaid_path,
|
|
2209
|
+
write_dag_json,
|
|
2210
|
+
write_dag_mermaid,
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
project_root = Path(project_path).resolve()
|
|
2214
|
+
default_output = default_dag_json_path(project_root) if output_format == "json" else default_dag_mermaid_path(project_root)
|
|
2215
|
+
output_path = Path(output).expanduser() if output else default_output
|
|
2216
|
+
if not output_path.is_absolute():
|
|
2217
|
+
output_path = project_root / output_path
|
|
2218
|
+
|
|
2219
|
+
if cache and output_path.exists():
|
|
2220
|
+
click.echo(f"Using cached DAG: {_display_path(output_path, project_root)}")
|
|
2221
|
+
return
|
|
2222
|
+
|
|
2223
|
+
try:
|
|
2224
|
+
built_dag = build_dag(project_root)
|
|
2225
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2226
|
+
click.echo(f"Error: {exc}")
|
|
2227
|
+
raise SystemExit(1)
|
|
2228
|
+
|
|
2229
|
+
if output_format == "json":
|
|
2230
|
+
if output_path != default_dag_json_path(project_root):
|
|
2231
|
+
write_dag_json(built_dag, project_root, output_path)
|
|
2232
|
+
else:
|
|
2233
|
+
write_dag_mermaid(built_dag, output_path)
|
|
2234
|
+
|
|
2235
|
+
click.echo(
|
|
2236
|
+
"Built DAG: "
|
|
2237
|
+
f"{len(built_dag.nodes)} nodes, "
|
|
2238
|
+
f"{len(built_dag.edges)} edges, "
|
|
2239
|
+
f"{len(built_dag.detect_cycles())} cycles -> "
|
|
2240
|
+
f"{_display_path(output_path, project_root)}"
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
@dag.command("verify")
|
|
2245
|
+
@click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
|
|
2246
|
+
@click.option("--check", "check_names", multiple=True, help="Run specific check(s) only")
|
|
2247
|
+
@click.option(
|
|
2248
|
+
"--format",
|
|
2249
|
+
"output_format",
|
|
2250
|
+
default="text",
|
|
2251
|
+
type=click.Choice(["text", "json"]),
|
|
2252
|
+
help="Output format",
|
|
2253
|
+
)
|
|
2254
|
+
def dag_verify(project_path: str, check_names: tuple[str, ...], output_format: str):
|
|
2255
|
+
"""Run DAG completeness checks."""
|
|
2256
|
+
from codd.dag.runner import run_all_checks
|
|
2257
|
+
|
|
2258
|
+
project_root = Path(project_path).resolve()
|
|
2259
|
+
try:
|
|
2260
|
+
results = run_all_checks(project_root, check_names=list(check_names) or None)
|
|
2261
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2262
|
+
click.echo(f"Error: {exc}")
|
|
2263
|
+
raise SystemExit(1)
|
|
2264
|
+
|
|
2265
|
+
failed_red = [
|
|
2266
|
+
result
|
|
2267
|
+
for result in results
|
|
2268
|
+
if not _dag_result_passed(result) and _dag_result_severity(result) == "red"
|
|
2269
|
+
]
|
|
2270
|
+
amber_findings = [
|
|
2271
|
+
result
|
|
2272
|
+
for result in results
|
|
2273
|
+
if _dag_result_severity(result) == "amber" and _dag_result_has_findings(result)
|
|
2274
|
+
]
|
|
2275
|
+
|
|
2276
|
+
if output_format == "json":
|
|
2277
|
+
click.echo(json.dumps([_dag_result_to_dict(result) for result in results], indent=2, default=str))
|
|
2278
|
+
else:
|
|
2279
|
+
for result in results:
|
|
2280
|
+
severity = _dag_result_severity(result)
|
|
2281
|
+
if _dag_result_passed(result):
|
|
2282
|
+
status = "PASS"
|
|
2283
|
+
else:
|
|
2284
|
+
status = "WARN" if severity == "amber" else "FAIL"
|
|
2285
|
+
click.echo(f" {status} {_dag_result_name(result)} [{severity}]")
|
|
2286
|
+
for detail in _dag_result_details(result):
|
|
2287
|
+
click.echo(f" {detail}")
|
|
2288
|
+
|
|
2289
|
+
if failed_red:
|
|
2290
|
+
click.echo(f"\n{len(failed_red)} check(s) FAILED (severity=red)")
|
|
2291
|
+
elif amber_findings:
|
|
2292
|
+
click.echo(f"\n{len(amber_findings)} check(s) WARN (severity=amber, deploy allowed)")
|
|
2293
|
+
|
|
2294
|
+
raise SystemExit(1 if failed_red else 0)
|
|
2295
|
+
|
|
2296
|
+
|
|
2297
|
+
@dag.command("visualize")
|
|
2298
|
+
@click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
|
|
2299
|
+
def dag_visualize(project_path: str):
|
|
2300
|
+
"""Build and print the project DAG as Mermaid."""
|
|
2301
|
+
from codd.dag.builder import build_dag, render_mermaid
|
|
2302
|
+
|
|
2303
|
+
project_root = Path(project_path).resolve()
|
|
2304
|
+
try:
|
|
2305
|
+
built_dag = build_dag(project_root)
|
|
2306
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
2307
|
+
click.echo(f"Error: {exc}")
|
|
2308
|
+
raise SystemExit(1)
|
|
2309
|
+
click.echo(render_mermaid(built_dag), nl=False)
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
def _dag_result_to_dict(result: Any) -> dict[str, Any]:
|
|
2313
|
+
if is_dataclass(result):
|
|
2314
|
+
return asdict(result)
|
|
2315
|
+
if isinstance(result, dict):
|
|
2316
|
+
return result
|
|
2317
|
+
return dict(vars(result))
|
|
2318
|
+
|
|
2319
|
+
|
|
2320
|
+
def _dag_result_name(result: Any) -> str:
|
|
2321
|
+
return str(_dag_result_value(result, "check_name") or result.__class__.__name__)
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
def _dag_result_severity(result: Any) -> str:
|
|
2325
|
+
return str(_dag_result_value(result, "severity") or "red")
|
|
2326
|
+
|
|
2327
|
+
|
|
2328
|
+
def _dag_result_passed(result: Any) -> bool:
|
|
2329
|
+
return _dag_result_value(result, "passed") is not False
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
def _dag_result_has_findings(result: Any) -> bool:
|
|
2333
|
+
for key in (
|
|
2334
|
+
"violations",
|
|
2335
|
+
"missing_impl_files",
|
|
2336
|
+
"orphan_edges",
|
|
2337
|
+
"dangling_refs",
|
|
2338
|
+
"incomplete_tasks",
|
|
2339
|
+
"unreachable_nodes",
|
|
2340
|
+
):
|
|
2341
|
+
value = _dag_result_value(result, key)
|
|
2342
|
+
if value:
|
|
2343
|
+
return True
|
|
2344
|
+
return False
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
def _dag_result_details(result: Any) -> list[str]:
|
|
2348
|
+
details: list[str] = []
|
|
2349
|
+
for key in (
|
|
2350
|
+
"missing_impl_files",
|
|
2351
|
+
"orphan_edges",
|
|
2352
|
+
"dangling_refs",
|
|
2353
|
+
"violations",
|
|
2354
|
+
"incomplete_tasks",
|
|
2355
|
+
"unreachable_nodes",
|
|
2356
|
+
"warnings",
|
|
2357
|
+
):
|
|
2358
|
+
value = _dag_result_value(result, key)
|
|
2359
|
+
if not value:
|
|
2360
|
+
continue
|
|
2361
|
+
if isinstance(value, list):
|
|
2362
|
+
rendered = ", ".join(str(item) for item in value[:5])
|
|
2363
|
+
if len(value) > 5:
|
|
2364
|
+
rendered += f", ... {len(value) - 5} more"
|
|
2365
|
+
details.append(f"{key}: {rendered}")
|
|
2366
|
+
else:
|
|
2367
|
+
details.append(f"{key}: {value}")
|
|
2368
|
+
return details
|
|
2369
|
+
|
|
2370
|
+
|
|
2371
|
+
def _dag_result_value(result: Any, key: str) -> Any:
|
|
2372
|
+
if isinstance(result, dict):
|
|
2373
|
+
return result.get(key)
|
|
2374
|
+
return getattr(result, key, None)
|
|
2375
|
+
|
|
2376
|
+
|
|
2377
|
+
@main.group()
|
|
2378
|
+
def hooks():
|
|
2379
|
+
"""Manage Git hook integration."""
|
|
2380
|
+
pass
|
|
2103
2381
|
|
|
2104
2382
|
|
|
2105
2383
|
@hooks.command("install")
|
|
@@ -2137,13 +2415,13 @@ def _render_template(template_name: str, dest: Path, variables: dict):
|
|
|
2137
2415
|
tmpl_path = TEMPLATES_DIR / template_name
|
|
2138
2416
|
if not tmpl_path.exists():
|
|
2139
2417
|
# Create empty file if template doesn't exist yet
|
|
2140
|
-
dest.write_text(f"# TODO: template {template_name} not yet created\n")
|
|
2418
|
+
dest.write_text(f"# TODO: template {template_name} not yet created\n", encoding="utf-8")
|
|
2141
2419
|
return
|
|
2142
2420
|
|
|
2143
|
-
content = tmpl_path.read_text()
|
|
2421
|
+
content = tmpl_path.read_text(encoding="utf-8")
|
|
2144
2422
|
for key, value in variables.items():
|
|
2145
2423
|
content = content.replace(f"{{{{{key}}}}}", value)
|
|
2146
|
-
dest.write_text(content)
|
|
2424
|
+
dest.write_text(content, encoding="utf-8")
|
|
2147
2425
|
|
|
2148
2426
|
|
|
2149
2427
|
def _import_requirements(project_root: Path, source: Path, project_name: str) -> Path:
|
|
@@ -80,6 +80,8 @@ def set_coherence_bus(bus: EventBus | None) -> None:
|
|
|
80
80
|
"""Set the opt-in coherence bus on detectors that publish DriftEvents."""
|
|
81
81
|
for module_name in (
|
|
82
82
|
"codd.drift",
|
|
83
|
+
"codd.drift_linkers.api",
|
|
84
|
+
"codd.deployer",
|
|
83
85
|
"codd.hitl_session",
|
|
84
86
|
"codd.validator",
|
|
85
87
|
"codd.screen_flow_validator",
|
|
@@ -129,7 +131,7 @@ class Orchestrator:
|
|
|
129
131
|
self._hitl_path = hitl_path
|
|
130
132
|
self._ntfy_rate_limit = ntfy_rate_limit_seconds
|
|
131
133
|
self._last_ntfy_time: float = 0.0
|
|
132
|
-
self.
|
|
134
|
+
self._pending_ntfy_events: list[DriftEvent] = []
|
|
133
135
|
bus.subscribe("*", self._handle)
|
|
134
136
|
|
|
135
137
|
def resolve_fix_strategy(self, event: DriftEvent) -> FixStrategy:
|
|
@@ -200,8 +202,9 @@ class Orchestrator:
|
|
|
200
202
|
handle.write("# Pending HITL Reviews\n")
|
|
201
203
|
handle.write(self._format_hitl_entry(event))
|
|
202
204
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
+
if event.severity == "red":
|
|
206
|
+
self._pending_ntfy_events.append(event)
|
|
207
|
+
self._maybe_send_ntfy(event)
|
|
205
208
|
|
|
206
209
|
def _format_hitl_entry(self, event: DriftEvent) -> str:
|
|
207
210
|
original_severity = event.payload.get("downgraded_from", event.severity)
|
|
@@ -223,17 +226,19 @@ class Orchestrator:
|
|
|
223
226
|
f"- **Reviewer**: (pending)\n"
|
|
224
227
|
)
|
|
225
228
|
|
|
226
|
-
def _maybe_send_ntfy(self) -> None:
|
|
227
|
-
"""Send at most one ntfy notification per rate-limit window."""
|
|
229
|
+
def _maybe_send_ntfy(self, event: DriftEvent) -> None:
|
|
230
|
+
"""Send at most one red-severity ntfy notification per rate-limit window."""
|
|
231
|
+
if event.severity != "red":
|
|
232
|
+
return
|
|
228
233
|
now = time.time()
|
|
229
234
|
if now - self._last_ntfy_time < self._ntfy_rate_limit:
|
|
230
235
|
return
|
|
231
|
-
count = len(self.
|
|
236
|
+
count = len(self._pending_ntfy_events)
|
|
232
237
|
if count == 0:
|
|
233
238
|
return
|
|
234
239
|
self._send_ntfy(f"CoDD Coherence: {count} HITL event(s) pending review")
|
|
235
240
|
self._last_ntfy_time = now
|
|
236
|
-
self.
|
|
241
|
+
self._pending_ntfy_events.clear()
|
|
237
242
|
|
|
238
243
|
def _send_ntfy(self, message: str) -> None:
|
|
239
244
|
"""Send an ntfy notification when NTFY_URL is configured."""
|
|
@@ -94,7 +94,7 @@ def build_interface_contracts(facts: ProjectFacts, project_root: Path) -> None:
|
|
|
94
94
|
if init_files:
|
|
95
95
|
init_path = project_root / init_files[0]
|
|
96
96
|
try:
|
|
97
|
-
init_content = init_path.read_text(errors="ignore")
|
|
97
|
+
init_content = init_path.read_text(encoding="utf-8", errors="ignore")
|
|
98
98
|
except Exception:
|
|
99
99
|
init_content = ""
|
|
100
100
|
public = detect_init_exports(init_content)
|
|
@@ -132,7 +132,7 @@ def compute_lexicon_compliance(project_root: Path | str, threshold: float = 100.
|
|
|
132
132
|
def compute_screen_flow_coverage(
|
|
133
133
|
project_root: Path | str,
|
|
134
134
|
config: dict[str, Any],
|
|
135
|
-
threshold: float =
|
|
135
|
+
threshold: float = 100.0,
|
|
136
136
|
) -> CoverageResult:
|
|
137
137
|
"""Measure screen-flow drift as a coverage gate metric."""
|
|
138
138
|
|
|
@@ -153,8 +153,25 @@ def compute_screen_flow_coverage(
|
|
|
153
153
|
details=[f"error: {exc}"],
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
design_drift_details: list[str] = []
|
|
157
|
+
design_drift_count = 0
|
|
158
|
+
try:
|
|
159
|
+
from codd.drift_linkers.screen_flow import ScreenFlowGate
|
|
160
|
+
|
|
161
|
+
gate_result = ScreenFlowGate(
|
|
162
|
+
project_root=project_root,
|
|
163
|
+
settings={**config, "apply": True},
|
|
164
|
+
).run()
|
|
165
|
+
if not gate_result.skipped:
|
|
166
|
+
design_drift_count = gate_result.drift_count
|
|
167
|
+
design_drift_details = gate_result.details
|
|
168
|
+
except Exception as exc: # pragma: no cover - defensive gate behavior
|
|
169
|
+
return _exception_result("screen_flow_coverage", threshold, exc)
|
|
170
|
+
|
|
171
|
+
drift_count = len(drifts) + design_drift_count
|
|
157
172
|
pct = 100.0 if drift_count == 0 else max(0.0, 100.0 - drift_count * 10.0)
|
|
173
|
+
details = [f"drift_count: {drift_count}"]
|
|
174
|
+
details.extend(design_drift_details)
|
|
158
175
|
return CoverageResult(
|
|
159
176
|
metric="screen_flow_coverage",
|
|
160
177
|
total=1,
|
|
@@ -163,7 +180,49 @@ def compute_screen_flow_coverage(
|
|
|
163
180
|
pct=pct,
|
|
164
181
|
threshold=threshold,
|
|
165
182
|
passed=pct >= threshold,
|
|
166
|
-
details=
|
|
183
|
+
details=details,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compute_dag_completeness(
|
|
188
|
+
project_root: Path | str,
|
|
189
|
+
config: dict[str, Any] | None = None,
|
|
190
|
+
threshold: float = 100.0,
|
|
191
|
+
) -> CoverageResult:
|
|
192
|
+
"""Measure red-severity DAG completeness checks as a coverage metric."""
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
from codd.dag.runner import run_all_checks
|
|
196
|
+
|
|
197
|
+
results = run_all_checks(Path(project_root), settings=config or {})
|
|
198
|
+
except Exception as exc: # pragma: no cover - defensive gate behavior
|
|
199
|
+
return _exception_result("dag_completeness", threshold, exc)
|
|
200
|
+
|
|
201
|
+
red_results = [result for result in results if _dag_result_severity(result) == "red"]
|
|
202
|
+
failed_red = [result for result in red_results if _dag_result_passed(result) is False]
|
|
203
|
+
amber_findings = [
|
|
204
|
+
result
|
|
205
|
+
for result in results
|
|
206
|
+
if _dag_result_severity(result) == "amber" and _dag_result_has_findings(result)
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
total = len(red_results)
|
|
210
|
+
uncovered = len(failed_red)
|
|
211
|
+
covered = max(0, total - uncovered)
|
|
212
|
+
pct = _coverage_pct(covered, total)
|
|
213
|
+
details = [f"checks: {len(results)}", f"red_failures: {uncovered}"]
|
|
214
|
+
details.extend(_format_dag_result(result) for result in failed_red[:5])
|
|
215
|
+
details.extend(f"warning: {_format_dag_result(result)}" for result in amber_findings[:5])
|
|
216
|
+
|
|
217
|
+
return CoverageResult(
|
|
218
|
+
metric="dag_completeness",
|
|
219
|
+
total=total,
|
|
220
|
+
covered=covered,
|
|
221
|
+
uncovered=uncovered,
|
|
222
|
+
pct=pct,
|
|
223
|
+
threshold=threshold,
|
|
224
|
+
passed=pct >= threshold,
|
|
225
|
+
details=details,
|
|
167
226
|
)
|
|
168
227
|
|
|
169
228
|
|
|
@@ -199,7 +258,7 @@ def run_coverage(
|
|
|
199
258
|
e2e_threshold: float = 100.0,
|
|
200
259
|
design_token_threshold: float = 0.0,
|
|
201
260
|
lexicon_threshold: float = 100.0,
|
|
202
|
-
screen_flow_threshold: float =
|
|
261
|
+
screen_flow_threshold: float = 100.0,
|
|
203
262
|
config: dict[str, Any] | None = None,
|
|
204
263
|
) -> CoverageReport:
|
|
205
264
|
"""Run all coverage metrics and return an aggregated report."""
|
|
@@ -213,6 +272,7 @@ def run_coverage(
|
|
|
213
272
|
report.add(compute_design_token_coverage(project_root, threshold=design_token_threshold))
|
|
214
273
|
report.add(compute_lexicon_compliance(project_root, threshold=lexicon_threshold))
|
|
215
274
|
report.add(compute_screen_flow_coverage(project_root, config, threshold=screen_flow_threshold))
|
|
275
|
+
report.add(compute_dag_completeness(project_root, config=config))
|
|
216
276
|
return report
|
|
217
277
|
|
|
218
278
|
|
|
@@ -307,3 +367,53 @@ def _exception_result(metric: str, threshold: float, exc: Exception) -> Coverage
|
|
|
307
367
|
passed=False,
|
|
308
368
|
details=[f"error: {type(exc).__name__}: {exc}"],
|
|
309
369
|
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _dag_result_severity(result: Any) -> str:
|
|
373
|
+
return str(_dag_result_value(result, "severity") or "red")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _dag_result_passed(result: Any) -> bool:
|
|
377
|
+
return _dag_result_value(result, "passed") is not False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _dag_result_name(result: Any) -> str:
|
|
381
|
+
return str(_dag_result_value(result, "check_name") or result.__class__.__name__)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _dag_result_has_findings(result: Any) -> bool:
|
|
385
|
+
for key in (
|
|
386
|
+
"violations",
|
|
387
|
+
"missing_impl_files",
|
|
388
|
+
"orphan_edges",
|
|
389
|
+
"dangling_refs",
|
|
390
|
+
"incomplete_tasks",
|
|
391
|
+
"unreachable_nodes",
|
|
392
|
+
):
|
|
393
|
+
if _dag_result_value(result, key):
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _format_dag_result(result: Any) -> str:
|
|
399
|
+
details = []
|
|
400
|
+
for key in (
|
|
401
|
+
"missing_impl_files",
|
|
402
|
+
"orphan_edges",
|
|
403
|
+
"dangling_refs",
|
|
404
|
+
"violations",
|
|
405
|
+
"incomplete_tasks",
|
|
406
|
+
"unreachable_nodes",
|
|
407
|
+
"warnings",
|
|
408
|
+
):
|
|
409
|
+
value = _dag_result_value(result, key)
|
|
410
|
+
if not value:
|
|
411
|
+
continue
|
|
412
|
+
details.append(f"{key}: {value}")
|
|
413
|
+
return f"{_dag_result_name(result)} ({'; '.join(details)})" if details else _dag_result_name(result)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _dag_result_value(result: Any, key: str) -> Any:
|
|
417
|
+
if isinstance(result, dict):
|
|
418
|
+
return result.get(key)
|
|
419
|
+
return getattr(result, key, None)
|