technical-debt-engine-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ """Code Size capability adapter backed by an explicitly installed cloc executable."""
2
+ from __future__ import annotations
3
+ import json, re, shutil, subprocess
4
+ from collections import defaultdict
5
+ from hashlib import sha256
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ CAPABILITY_ID = "code_size"
10
+ CAPABILITY_VERSION = "0.1.0"
11
+ ADAPTER_ID = "code_size.cloc"
12
+ ADAPTER_VERSION = "0.1.0"
13
+ MINIMUM_ANALYZER_VERSION = (2, 10)
14
+
15
+ def classify(path: str) -> str:
16
+ value = path.replace("\\", "/").lower()
17
+ if value.startswith(("vendor/", "third_party/", "node_modules/")) or any(part in value for part in ("/vendor/", "/third_party/", "/node_modules/")): return "VENDOR"
18
+ if value.startswith(("generated/", "build/", "dist/")) or any(part in value for part in ("/generated/", "/build/", "/dist/")): return "GENERATED"
19
+ if value.startswith(("tests/", "test/", "spec/")) or "/tests/" in value: return "TEST"
20
+ if value.startswith(("docs/", "documentation/")) or value.endswith((".md", ".rst", ".txt")): return "DOCUMENTATION"
21
+ if value.endswith((".yml", ".yaml", ".json", ".toml", ".ini")): return "CONFIGURATION"
22
+ return "SOURCE"
23
+
24
+ def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
25
+ executable = shutil.which("cloc")
26
+ if not executable:
27
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.unavailable","description":"cloc is not on PATH; install cloc 2.10+.","cause":"analyzer unavailable"}]}
28
+ try:
29
+ version = subprocess.run([executable, "--version"], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip()
30
+ match = re.search(r"(\d+)\.(\d+)", version)
31
+ if not match or tuple(map(int, match.groups())) < MINIMUM_ANALYZER_VERSION:
32
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.unsupported_version","description":f"cloc {MINIMUM_ANALYZER_VERSION[0]}.{MINIMUM_ANALYZER_VERSION[1]}+ is required; found {version or 'unknown'}.","cause":"unsupported analyzer version"}]}
33
+ result = subprocess.run([executable, "--json", "--by-file", "--quiet", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
34
+ raw = result.stdout
35
+ data = json.loads(raw)
36
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
37
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.failed","description":str(error),"cause":"analyzer execution failed"}]}
38
+ files, languages = [], defaultdict(lambda: {"files":0,"code":0,"comment":0,"blank":0})
39
+ totals = defaultdict(int)
40
+ entries = ((name, item) for name, item in data.items() if name not in {"header", "SUM"})
41
+ for name, item in sorted(entries):
42
+ relative = Path(name).resolve().relative_to(root.resolve()).as_posix()
43
+ category, language = classify(relative), item.get("language", "Unknown")
44
+ record = {"path":relative,"classification":category,"language":language,"physical":item["blank"]+item["comment"]+item["code"],"code":item["code"],"comment":item["comment"],"blank":item["blank"]}
45
+ files.append(record); bucket = languages[language]; bucket["files"] += 1; bucket["code"] += item["code"]; bucket["comment"] += item["comment"]; bucket["blank"] += item["blank"]
46
+ for key in ("code","comment","blank"): totals[key] += item[key]
47
+ totals["files"] += 1; totals[category.lower()] += item["code"]
48
+ source = totals["source"]; ratio = totals["test"] / source if source else 0
49
+ return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"cloc","version":version},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"files":files,"languages":dict(sorted(languages.items())),"totals":dict(totals),"testToSourceRatio":ratio,"limitations":[{"id":"code_size.logical_lines.unavailable","description":"cloc does not provide logical line counts.","cause":"analyzer limitation"}]}
@@ -0,0 +1,92 @@
1
+ """Python cyclomatic-complexity adapter backed by the installed Radon CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import json
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ from hashlib import sha256
11
+ from pathlib import Path
12
+ from typing import Any, Mapping
13
+
14
+ CAPABILITY_ID = "complexity"
15
+ CAPABILITY_VERSION = "0.1.0"
16
+ ADAPTER_ID = "complexity.radon"
17
+ ADAPTER_VERSION = "0.1.0"
18
+ MINIMUM_ANALYZER_VERSION = (6, 0)
19
+
20
+ def _items(value: object) -> tuple[str, ...]:
21
+ if isinstance(value, str):
22
+ return tuple(item.strip() for item in value.split(",") if item.strip())
23
+ if isinstance(value, (list, tuple)) and all(isinstance(item, str) for item in value):
24
+ return tuple(value)
25
+ return ()
26
+
27
+ def _relative(root: Path, name: str) -> str:
28
+ path = Path(name)
29
+ if not path.is_absolute():
30
+ return path.as_posix()
31
+ try:
32
+ return path.resolve().relative_to(root.resolve()).as_posix()
33
+ except ValueError:
34
+ # Radon is expected to report files below the selected root. Never
35
+ # preserve an absolute host path in evidence if an analyzer violates
36
+ # that expectation.
37
+ return path.name
38
+
39
+
40
+ def _portable_native_output(root: Path, data: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
41
+ """Return a stable Radon projection without runner-specific paths."""
42
+ normalized = {_relative(root, path): symbols for path, symbols in data.items()}
43
+ ordered = {path: normalized[path] for path in sorted(normalized)}
44
+ return json.dumps(ordered, sort_keys=True, separators=(",", ":")), ordered
45
+
46
+ def _thresholds(configuration: Mapping[str, Any]) -> dict[str, int]:
47
+ supplied = configuration.get("thresholds", {})
48
+ if not isinstance(supplied, Mapping): supplied = {}
49
+ values = {"high": 11, "veryHigh": 21, "critical": 41}
50
+ for key in values:
51
+ if key in supplied:
52
+ if not isinstance(supplied[key], int) or supplied[key] < 1:
53
+ raise ValueError(f"complexity threshold {key} must be a positive integer")
54
+ values[key] = supplied[key]
55
+ if not values["high"] < values["veryHigh"] < values["critical"]:
56
+ raise ValueError("complexity thresholds must satisfy high < veryHigh < critical")
57
+ return values
58
+
59
+ def analyze(root: Path, timeout: int = 60, configuration: Mapping[str, Any] | None = None) -> dict[str, Any]:
60
+ """Execute Radon deterministically and retain native output for evidence."""
61
+ configuration = configuration or {}
62
+ executable = shutil.which("radon")
63
+ if not executable:
64
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.unavailable","description":"radon is not on PATH; install Radon 6.0+.","cause":"analyzer unavailable"}]}
65
+ try:
66
+ version = subprocess.run([executable, "--version"], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip()
67
+ match = re.search(r"(\d+)\.(\d+)", version)
68
+ if not match or tuple(map(int, match.groups())) < MINIMUM_ANALYZER_VERSION:
69
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.unsupported_version","description":f"Radon 6.0+ is required; found {version or 'unknown'}.","cause":"unsupported analyzer version"}]}
70
+ completed = subprocess.run([executable, "cc", "--json", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
71
+ raw, data = _portable_native_output(root, json.loads(completed.stdout))
72
+ except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
73
+ return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.failed","description":str(error),"cause":"analyzer execution failed"}]}
74
+ try:
75
+ thresholds = _thresholds(configuration)
76
+ except ValueError as error:
77
+ return {"status":"BLOCKED", "limitations":[{"id":"complexity.configuration.invalid","description":str(error),"cause":"invalid configuration"}]}
78
+ defaults = (".git/**", ".tde/**", "build/**", "dist/**", "*.egg-info/**", ".venv/**", "venv/**", "__pycache__/**")
79
+ ignored_paths, ignored_symbols = defaults + _items(configuration.get("ignoredPaths")), set(_items(configuration.get("ignoredSymbols")))
80
+ symbols, skipped = [], 0
81
+ for native_path, native_symbols in sorted(data.items()):
82
+ path = _relative(root, native_path)
83
+ if any(fnmatch.fnmatch(path, pattern) or path.startswith(pattern.rstrip("/")+"/") for pattern in ignored_paths):
84
+ skipped += len(native_symbols); continue
85
+ for native in sorted(native_symbols, key=lambda item: (item.get("lineno", 0), item.get("name", ""))):
86
+ if native.get("name") in ignored_symbols:
87
+ skipped += 1; continue
88
+ symbols.append({"path":path,"language":"Python","name":native["name"],"type":native.get("type","symbol"),"line":native.get("lineno"),"endLine":native.get("endline"),"complexity":native["complexity"]})
89
+ limitations=[]
90
+ if skipped: limitations.append({"id":"complexity.configuration.ignored","description":f"{skipped} symbol(s) were excluded by Complexity configuration.","cause":"configured exclusion"})
91
+ if not symbols: limitations.append({"id":"complexity.python.no_symbols","description":"Radon found no supported Python symbols after exclusions.","cause":"analyzer capability limitation"})
92
+ return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"radon","version":version},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"symbols":symbols,"thresholds":thresholds,"limitations":limitations}
@@ -0,0 +1,112 @@
1
+ """Canonical Runtime configuration loading and repository discovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from hashlib import sha256
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any, Mapping
10
+
11
+
12
+ def _scalar(value: str) -> Any:
13
+ value = value.strip()
14
+ if value in {"true", "True"}:
15
+ return True
16
+ if value in {"false", "False"}:
17
+ return False
18
+ if value in {"null", "Null", "~"}:
19
+ return None
20
+ if value.startswith(("\"", "'")) and value.endswith(("\"", "'")):
21
+ return value[1:-1]
22
+ try:
23
+ return int(value)
24
+ except ValueError:
25
+ return value
26
+
27
+
28
+ def _parse_yaml(contents: str) -> dict[str, Any]:
29
+ """Parse the small mapping-only .tde.yml contract without a runtime dependency."""
30
+ root: dict[str, Any] = {}
31
+ stack: list[tuple[int, dict[str, Any]]] = [(-1, root)]
32
+ for raw in contents.splitlines():
33
+ if not raw.strip() or raw.lstrip().startswith("#"):
34
+ continue
35
+ indent = len(raw) - len(raw.lstrip(" "))
36
+ key, separator, value = raw.strip().partition(":")
37
+ if not separator or not key:
38
+ raise ValueError(".tde.yml supports mappings only")
39
+ while stack[-1][0] >= indent:
40
+ stack.pop()
41
+ parent = stack[-1][1]
42
+ if value.strip():
43
+ parent[key] = _scalar(value)
44
+ else:
45
+ nested: dict[str, Any] = {}
46
+ parent[key] = nested
47
+ stack.append((indent, nested))
48
+ return root
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class RuntimeConfiguration:
53
+ schema_version: str = "1.0.0"
54
+ execution_options: dict[str, Any] | None = None
55
+
56
+ @classmethod
57
+ def load(cls, values: Mapping[str, Any] | None = None) -> "RuntimeConfiguration":
58
+ values = {} if values is None else dict(values)
59
+ allowed = {"schemaVersion", "executionOptions", "capabilities", "policy", "baseline", "trend"}
60
+ unknown = set(values) - allowed
61
+ if unknown:
62
+ raise ValueError(f"unsupported runtime configuration: {sorted(unknown)}")
63
+ schema_version = values.get("schemaVersion", "1.0.0")
64
+ if schema_version != "1.0.0":
65
+ raise ValueError("unsupported configuration schema version")
66
+ options = values.get("executionOptions", {})
67
+ if not isinstance(options, dict):
68
+ raise ValueError("executionOptions must be an object")
69
+ resolved: dict[str, Any] = dict(options)
70
+ for key in ("capabilities", "policy", "baseline", "trend"):
71
+ value = values.get(key, resolved.get(key, {}))
72
+ if not isinstance(value, dict):
73
+ raise ValueError(f"{key} must be an object")
74
+ resolved[key] = value
75
+ return cls(schema_version=schema_version, execution_options=resolved)
76
+
77
+ @classmethod
78
+ def discover(cls, repository_root: str | Path, explicit_path: str | Path | None = None) -> "RuntimeConfiguration":
79
+ path = Path(explicit_path) if explicit_path else Path(repository_root) / ".tde.yml"
80
+ if not path.is_file():
81
+ if explicit_path:
82
+ raise ValueError(f"configuration file does not exist: {path}")
83
+ return cls.load()
84
+ try:
85
+ contents = path.read_text(encoding="utf-8")
86
+ try:
87
+ values = json.loads(contents)
88
+ except json.JSONDecodeError:
89
+ values = _parse_yaml(contents)
90
+ except (OSError, ValueError) as error:
91
+ raise ValueError(f"invalid configuration {path}: {error}") from error
92
+ if not isinstance(values, dict):
93
+ raise ValueError("configuration root must be an object")
94
+ return cls.load(values)
95
+
96
+ def with_capability(self, identifier: str, enabled: bool = True) -> "RuntimeConfiguration":
97
+ values = self.as_dict()
98
+ capabilities = dict(values["executionOptions"].get("capabilities", {}))
99
+ existing = capabilities.get(identifier, {})
100
+ if not isinstance(existing, dict):
101
+ raise ValueError(f"capability configuration for {identifier} must be an object")
102
+ capabilities[identifier] = {**existing, "enabled": enabled}
103
+ values["capabilities"] = capabilities
104
+ values["executionOptions"].pop("capabilities", None)
105
+ return self.load(values)
106
+
107
+ def as_dict(self) -> dict[str, Any]:
108
+ return {"schemaVersion": self.schema_version, "executionOptions": self.execution_options or {}}
109
+
110
+ def digest(self) -> str:
111
+ canonical = json.dumps(self.as_dict(), sort_keys=True, separators=(",", ":"))
112
+ return f"sha256:{sha256(canonical.encode()).hexdigest()}"
@@ -0,0 +1,23 @@
1
+ """Declarative dependency inventory adapter; does not execute package managers."""
2
+ from __future__ import annotations
3
+ import json, tomllib
4
+ from pathlib import Path
5
+ from typing import Any
6
+ CAPABILITY_ID="dependency_health"; CAPABILITY_VERSION="0.1.0"
7
+ def discover(root: Path) -> dict[str, Any]:
8
+ inventory=[]
9
+ requirements=root/"requirements.txt"
10
+ if requirements.exists():
11
+ for line in requirements.read_text().splitlines():
12
+ name=line.split("==")[0].strip()
13
+ if name and not name.startswith("#"): inventory.append({"identifier":f"pypi:{name}","displayName":name,"ecosystem":"Python","packageManager":"requirements.txt","declaredVersion":line.partition("==")[2] or None,"lockState":"DECLARED"})
14
+ pyproject=root/"pyproject.toml"
15
+ if pyproject.exists():
16
+ data=tomllib.loads(pyproject.read_text()); deps=data.get("project",{}).get("dependencies",[])
17
+ for dep in deps:
18
+ name=dep.split()[0].split(">")[0].split("=")[0]; inventory.append({"identifier":f"pypi:{name}","displayName":name,"ecosystem":"Python","packageManager":"pyproject.toml","declaredVersion":dep,"lockState":"DECLARED"})
19
+ package=root/"package.json"
20
+ if package.exists():
21
+ data=json.loads(package.read_text())
22
+ for name,version in data.get("dependencies",{}).items(): inventory.append({"identifier":f"npm:{name}","displayName":name,"ecosystem":"JavaScript","packageManager":"package.json","declaredVersion":version,"lockState":"DECLARED"})
23
+ return {"status":"VALID","inventory":inventory,"measurements":[{"measurementId":"dependency_health.repository.count","capabilityId":CAPABILITY_ID,"metricKey":"dependency.count","value":len(inventory),"unit":"packages","scope":"repository","targetEntityId":"repository","aggregation":"count","sourceAdapterId":"dependency_health.declarative","sourceToolId":"filesystem"}],"findings":[],"limitations":[]}
@@ -0,0 +1,47 @@
1
+ """Fail-closed validation for a preserved candidate-bound OCI archive."""
2
+ from __future__ import annotations
3
+
4
+ from hashlib import sha256
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+
9
+
10
+ def digest(path: str | Path) -> str:
11
+ value = Path(path)
12
+ hasher = sha256()
13
+ with value.open("rb") as stream:
14
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
15
+ hasher.update(chunk)
16
+ return "sha256:" + hasher.hexdigest()
17
+
18
+
19
+ def canonical(value: Mapping[str, Any]) -> bytes:
20
+ return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("utf-8")
21
+
22
+
23
+ def validate(directory: str | Path, candidate_sha: str) -> dict[str, Any]:
24
+ """Return a Docker artifact record only when archive and provenance bind."""
25
+ root = Path(directory).resolve()
26
+ archive = root / "tde-oci.tar"
27
+ provenance_path = root / "docker-provenance.json"
28
+ try:
29
+ provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
30
+ archive_digest = digest(archive)
31
+ platforms = provenance["platforms"]
32
+ valid = (
33
+ provenance.get("schemaId") == "tde.docker-provenance"
34
+ and provenance.get("schemaVersion") == "1.0.0"
35
+ and provenance.get("candidateSha") == candidate_sha
36
+ and provenance.get("ociArchive", {}).get("digest") == archive_digest
37
+ and provenance.get("baseImage", {}).get("digest", "").startswith("sha256:")
38
+ and provenance.get("wheel", {}).get("digest", "").startswith("sha256:")
39
+ and provenance.get("dockerfile", {}).get("digest", "").startswith("sha256:")
40
+ and isinstance(platforms, list) and {item.get("platform") for item in platforms} >= {"linux/amd64", "linux/arm64"}
41
+ and all(str(item.get("digest", "")).startswith("sha256:") for item in platforms)
42
+ and str(provenance.get("ociIndex", {}).get("digest", "")).startswith("sha256:")
43
+ )
44
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
45
+ provenance, archive_digest, valid = {}, None, False
46
+ return {"filename": archive.name, "digest": archive_digest, "kind": "oci_archive",
47
+ "candidateSha": candidate_sha, "provenance": provenance, "verified": valid}
@@ -0,0 +1,69 @@
1
+ """Storage-technology-independent immutable canonical evidence repository."""
2
+ from __future__ import annotations
3
+ from hashlib import sha256
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ class EvidenceStore:
9
+ def __init__(self, location: str | Path) -> None: self.location = Path(location)
10
+
11
+ @staticmethod
12
+ def _identity(evidence: Mapping[str, Any]) -> str:
13
+ digest = evidence.get("integrity", {}).get("contentDigest", "")
14
+ if not isinstance(digest, str) or not digest.startswith("sha256:"):
15
+ raise ValueError("evidence integrity digest is missing")
16
+ return digest.removeprefix("sha256:")
17
+
18
+ @staticmethod
19
+ def _calculated_identity(evidence: Mapping[str, Any]) -> str:
20
+ stable_results = [{key: value for key, value in result.items() if key != "executionTiming"}
21
+ for result in evidence.get("capabilityResults", [])]
22
+ seed = json.dumps({"repository": evidence.get("repository", {}).get("id"),
23
+ "candidate": evidence.get("candidate"),
24
+ "configuration": evidence.get("configurationDigest"),
25
+ "capabilityResults": stable_results,
26
+ "measurements": evidence.get("measurements", []),
27
+ "findings": evidence.get("findings", []),
28
+ "policy": evidence.get("policyEvidence", {})},
29
+ sort_keys=True, separators=(",", ":"), default=str)
30
+ return sha256(seed.encode()).hexdigest()
31
+
32
+ @classmethod
33
+ def _validate(cls, evidence: Mapping[str, Any], identity: str | None = None) -> str:
34
+ if evidence.get("schemaId") != "tde.evidence" or evidence.get("validation", {}).get("status") != "VALID":
35
+ raise ValueError("store requires validated canonical evidence")
36
+ actual = cls._identity(evidence)
37
+ if identity and identity != actual:
38
+ raise ValueError("persisted evidence identity does not match its record")
39
+ if cls._calculated_identity(evidence) != actual:
40
+ raise ValueError("persisted evidence integrity check failed")
41
+ return actual
42
+
43
+ def persist(self, evidence: Mapping[str, Any], kind: str = "evidence") -> dict[str, Any]:
44
+ identity = self._validate(evidence)
45
+ path = self.location / kind / f"{identity}.json"
46
+ if path.exists(): return {"id": identity, "kind": kind, "path": str(path), "immutable": True, "existing": True}
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ record = {"kind": kind, "repository": evidence["repository"]["id"], "candidate": evidence["candidate"]["id"], "runtime": evidence["runtime"]["version"], "schema": evidence["schemaVersion"], "timestamp": evidence["timestamps"]["generatedAt"], "qualification": evidence.get("policyEvidence", {}).get("decision"), "evidence": evidence}
49
+ path.write_text(json.dumps(record, sort_keys=True, indent=2) + "\n", encoding="utf-8")
50
+ return {"id": identity, "kind": kind, "path": str(path), "immutable": True, "existing": False}
51
+
52
+ def retrieve(self, identity: str, kind: str = "evidence") -> dict[str, Any]:
53
+ path = self.location / kind / f"{identity}.json"
54
+ try:
55
+ record = json.loads(path.read_text(encoding="utf-8"))
56
+ except (OSError, json.JSONDecodeError) as error:
57
+ raise ValueError(f"persisted evidence is unavailable: {identity}") from error
58
+ if record.get("kind") != kind or not isinstance(record.get("evidence"), dict):
59
+ raise ValueError("persisted evidence record is malformed")
60
+ self._validate(record["evidence"], identity)
61
+ return record
62
+
63
+ def history(self, kind: str = "evidence") -> list[dict[str, Any]]:
64
+ directory = self.location / kind
65
+ records=[]
66
+ for path in sorted(directory.glob("*.json")) if directory.is_dir() else []:
67
+ identity = path.stem
68
+ records.append(self.retrieve(identity, kind))
69
+ return sorted(records, key=lambda item: item["timestamp"])
@@ -0,0 +1,213 @@
1
+ """Sequential, registry-driven coordinator for registered capability execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from time import perf_counter
6
+ from typing import Any
7
+ from hashlib import sha256
8
+
9
+ from .code_size import ADAPTER_ID, CAPABILITY_ID, CAPABILITY_VERSION, analyze
10
+ from .complexity import ADAPTER_ID as COMPLEXITY_ADAPTER_ID, CAPABILITY_ID as COMPLEXITY_CAPABILITY_ID, CAPABILITY_VERSION as COMPLEXITY_CAPABILITY_VERSION, analyze as analyze_complexity
11
+ from .dependency_health import CAPABILITY_ID as DEPENDENCY_CAPABILITY_ID, CAPABILITY_VERSION as DEPENDENCY_CAPABILITY_VERSION, discover as discover_dependencies
12
+ from .maintainability import CAPABILITY_ID as MAINTAINABILITY_CAPABILITY_ID, CAPABILITY_VERSION as MAINTAINABILITY_CAPABILITY_VERSION, derive as derive_maintainability
13
+ from .registries import AdapterRegistry, CapabilityRegistry
14
+
15
+
16
+ class CapabilityExecutionEngine:
17
+ """Plans and records capability work; Runtime only consumes its canonical output."""
18
+
19
+ states = ("PLANNED", "READY", "RUNNING", "COMPLETED", "FAILED", "BLOCKED", "SKIPPED", "NOT_SUPPORTED")
20
+
21
+ def __init__(self, capability_registry: CapabilityRegistry | None = None,
22
+ adapter_registry: AdapterRegistry | None = None) -> None:
23
+ self._capability_registry = capability_registry or CapabilityRegistry()
24
+ self._adapter_registry = adapter_registry or AdapterRegistry()
25
+
26
+ def plan(self, context: Any) -> dict[str, Any]:
27
+ requested = context.execution_options.get("capabilities", {})
28
+ available = {item["id"]: item for item in self._capability_registry.discover()}
29
+ enabled = [identifier for identifier, settings in requested.items() if settings.get("enabled")]
30
+ requested_plan = [identifier for identifier in enabled if identifier in available]
31
+ planned = [identifier for identifier in (CAPABILITY_ID, COMPLEXITY_CAPABILITY_ID, MAINTAINABILITY_CAPABILITY_ID, DEPENDENCY_CAPABILITY_ID)
32
+ if identifier in requested_plan or (identifier in {CAPABILITY_ID, COMPLEXITY_CAPABILITY_ID} and MAINTAINABILITY_CAPABILITY_ID in requested_plan)]
33
+ unsupported = [identifier for identifier in enabled if identifier not in available]
34
+ adapters = {item["id"] for item in self._adapter_registry.discover()}
35
+ planned_adapters = [adapter for identifier, adapter in ((CAPABILITY_ID, ADAPTER_ID), (COMPLEXITY_CAPABILITY_ID, COMPLEXITY_ADAPTER_ID))
36
+ if identifier in planned and adapter in adapters]
37
+ return {
38
+ "state": "PLANNED",
39
+ "capabilities": planned,
40
+ "unsupportedCapabilities": unsupported,
41
+ "plannedAdapters": planned_adapters,
42
+ "parallelReady": True,
43
+ "retries": "NONE",
44
+ }
45
+
46
+ def execute(self, context: Any) -> dict[str, Any]:
47
+ started = perf_counter()
48
+ plan = self.plan(context)
49
+ evidence = self._execution_evidence(context, plan)
50
+ measurements: list[dict[str, Any]] = []
51
+ findings: list[dict[str, Any]] = []
52
+ capability_results: list[dict[str, Any]] = []
53
+ adapter_results: list[dict[str, Any]] = []
54
+
55
+ for identifier in plan["capabilities"]:
56
+ normalized = self._dispatch(identifier, context, measurements)
57
+ measurements.extend(normalized["measurements"])
58
+ findings.extend(normalized["findings"])
59
+ capability_results.extend(normalized["capabilityResults"])
60
+ adapter_results.extend(normalized.get("adapterResults", []))
61
+ result = normalized["capabilityResults"][-1]
62
+ adapter_ids = result.get("adapterIds", [])
63
+ state = "COMPLETED" if result["status"] == "VALID" else "BLOCKED"
64
+ if state == "COMPLETED":
65
+ evidence["executedCapabilities"].append(identifier)
66
+ evidence["executedAdapters"].extend(adapter_ids)
67
+ else:
68
+ evidence["blockedCapabilities"].append(identifier)
69
+ evidence["limitations"].extend(result.get("limitations", []))
70
+ evidence["workItems"].append({"capabilityId": identifier, "adapterId": adapter_ids[0] if adapter_ids else None,
71
+ "state": state, "durationMs": result["executionTiming"]["durationMs"]})
72
+
73
+ evidence["unsupportedCapabilities"].extend(plan["unsupportedCapabilities"])
74
+ evidence["durationMs"] = int((perf_counter() - started) * 1000)
75
+ evidence["state"] = "COMPLETED" if evidence["executedCapabilities"] else "BLOCKED"
76
+ return {
77
+ "executedWorkItems": len(evidence["workItems"]),
78
+ "measurements": measurements,
79
+ "findings": findings,
80
+ "capabilityResults": capability_results,
81
+ "adapterResults": adapter_results,
82
+ "executionEvidence": evidence,
83
+ }
84
+
85
+ def _dispatch(self, identifier: str, context: Any, measurements: list[dict[str, Any]]) -> dict[str, Any]:
86
+ started = perf_counter()
87
+ timeout = int(context.execution_options.get("timeout", 60))
88
+ if identifier == CAPABILITY_ID:
89
+ result = analyze(context.repository_root, timeout)
90
+ duration = int((perf_counter() - started) * 1000)
91
+ return self._code_size_result(context, result, duration) if result["status"] == "VALID" else self._blocked(CAPABILITY_ID, CAPABILITY_VERSION, [ADAPTER_ID], result["limitations"], duration)
92
+ if identifier == COMPLEXITY_CAPABILITY_ID:
93
+ settings = context.execution_options.get("capabilities", {}).get(COMPLEXITY_CAPABILITY_ID, {})
94
+ result = analyze_complexity(context.repository_root, timeout, settings)
95
+ duration = int((perf_counter() - started) * 1000)
96
+ if result["status"] != "VALID":
97
+ return self._blocked(COMPLEXITY_CAPABILITY_ID, COMPLEXITY_CAPABILITY_VERSION, [COMPLEXITY_ADAPTER_ID], result["limitations"], duration)
98
+ return self._complexity_result(context, result, duration)
99
+ if identifier == DEPENDENCY_CAPABILITY_ID:
100
+ result = discover_dependencies(context.repository_root)
101
+ duration = int((perf_counter() - started) * 1000)
102
+ capability = {"capabilityId": DEPENDENCY_CAPABILITY_ID, "capabilityVersion": DEPENDENCY_CAPABILITY_VERSION,
103
+ "status": "VALID", "adapterIds": [], "completeness": 1, "qualificationApplicable": True,
104
+ "executionTiming": {"durationMs": duration}}
105
+ return {"measurements": result["measurements"], "findings": result["findings"], "capabilityResults": [capability]}
106
+ code = {"measurements": measurements}
107
+ complexity = {"measurements": [item for item in measurements if item.get("capabilityId") == COMPLEXITY_CAPABILITY_ID]}
108
+ result = derive_maintainability(code, complexity)
109
+ duration = int((perf_counter() - started) * 1000)
110
+ if result["status"] != "VALID":
111
+ return self._blocked(MAINTAINABILITY_CAPABILITY_ID, MAINTAINABILITY_CAPABILITY_VERSION, [], result["limitations"], duration)
112
+ capability = {"capabilityId": MAINTAINABILITY_CAPABILITY_ID, "capabilityVersion": MAINTAINABILITY_CAPABILITY_VERSION,
113
+ "status": "VALID", "adapterIds": [], "completeness": 1, "qualificationApplicable": True,
114
+ "executionTiming": {"durationMs": duration}}
115
+ return {"measurements": result["measurements"], "findings": result["findings"], "capabilityResults": [capability]}
116
+
117
+ @staticmethod
118
+ def _blocked(identifier: str, version: str, adapter_ids: list[str], limitations: list[dict[str, Any]], duration: int) -> dict[str, Any]:
119
+ return {"measurements": [], "findings": [], "capabilityResults": [{"capabilityId": identifier, "capabilityVersion": version,
120
+ "status": "BLOCKED", "adapterIds": adapter_ids, "completeness": 0, "qualificationApplicable": False,
121
+ "limitations": limitations, "executionTiming": {"durationMs": duration}}]}
122
+
123
+ @staticmethod
124
+ def _execution_evidence(context: Any, plan: dict[str, Any]) -> dict[str, Any]:
125
+ return {
126
+ "executionId": context.execution_id,
127
+ "plannedCapabilities": plan["capabilities"],
128
+ "executedCapabilities": [],
129
+ "skippedCapabilities": [],
130
+ "blockedCapabilities": [],
131
+ "unsupportedCapabilities": [],
132
+ "plannedAdapters": plan["plannedAdapters"],
133
+ "executedAdapters": [],
134
+ "workItems": [],
135
+ "executionGraph": {"nodes": plan["capabilities"], "edges": []},
136
+ "durationMs": 0,
137
+ "state": "PLANNED",
138
+ "limitations": [],
139
+ }
140
+
141
+ @staticmethod
142
+ def _code_size_result(context: Any, result: dict[str, Any], duration: int) -> dict[str, Any]:
143
+ names = {"files": "file_count", "code": "code_lines", "comment": "comment_lines", "blank": "blank_lines",
144
+ "source": "source_lines", "test": "test_lines", "generated": "generated_lines",
145
+ "vendor": "vendor_lines", "documentation": "documentation_lines"}
146
+ measurements = [
147
+ {"measurementId": f"code_size.repository.{key}", "capabilityId": CAPABILITY_ID,
148
+ "metricKey": f"code_size.{names[key]}", "value": value, "unit": "files" if key == "files" else "lines",
149
+ "scope": "repository", "targetEntityId": context.repository_id, "aggregation": "sum",
150
+ "sourceAdapterId": result["adapter"]["id"], "sourceToolId": "cloc"}
151
+ for key, value in result["totals"].items() if key in names
152
+ ]
153
+ measurements.append({"measurementId": "code_size.repository.test_ratio", "capabilityId": CAPABILITY_ID,
154
+ "metricKey": "code_size.test_to_source_ratio", "value": result["testToSourceRatio"],
155
+ "unit": "ratio", "scope": "repository", "targetEntityId": context.repository_id,
156
+ "aggregation": "ratio", "sourceAdapterId": result["adapter"]["id"], "sourceToolId": "cloc"})
157
+ for language, totals in result["languages"].items():
158
+ language_id = f"language.{language.lower().replace(' ', '_')}"
159
+ for key in ("files", "code", "comment", "blank"):
160
+ measurements.append({"measurementId": f"code_size.{language_id}.{key}", "capabilityId": CAPABILITY_ID,
161
+ "metricKey": f"code_size.language_{key}", "value": totals[key],
162
+ "unit": "files" if key == "files" else "lines", "scope": "language",
163
+ "targetEntityId": language_id, "aggregation": "sum",
164
+ "sourceAdapterId": result["adapter"]["id"], "sourceToolId": "cloc"})
165
+ for file in result["files"]:
166
+ file_id = "file." + sha256(file["path"].encode()).hexdigest()[:16]
167
+ for key in ("code", "comment", "blank"):
168
+ measurements.append({"measurementId": f"code_size.{file_id}.{key}", "capabilityId": CAPABILITY_ID,
169
+ "metricKey": f"code_size.file_{key}_lines", "value": file[key], "unit": "lines",
170
+ "scope": "file", "targetEntityId": file_id, "aggregation": "sum",
171
+ "sourceAdapterId": result["adapter"]["id"], "sourceToolId": "cloc"})
172
+ adapter_result = {"adapter": result["adapter"], "analyzer": result["analyzer"], "execution": "SUCCESS",
173
+ "rawOutputHash": result["rawOutputHash"], "rawOutput": result["rawOutput"],
174
+ "measuredScope": ["repository", "language", "file"], "completeness": 1,
175
+ "draftMeasurements": measurements, "draftFindings": [], "warnings": [], "errors": [],
176
+ "limitations": result["limitations"], "executionTiming": {"durationMs": duration}}
177
+ return {"measurements": measurements, "findings": [], "adapterResults": [adapter_result], "capabilityResults": [
178
+ {"capabilityId": CAPABILITY_ID, "capabilityVersion": CAPABILITY_VERSION, "status": "VALID",
179
+ "adapterIds": [result["adapter"]["id"]], "completeness": 1, "qualificationApplicable": True,
180
+ "limitations": result["limitations"], "executionTiming": {"durationMs": duration}}
181
+ ]}
182
+
183
+ @staticmethod
184
+ def _complexity_result(context: Any, result: dict[str, Any], duration: int) -> dict[str, Any]:
185
+ symbols, measurements, findings = result["symbols"], [], []
186
+ def measurement_id(scope: str, entity: str, metric: str) -> str:
187
+ return f"complexity.{scope}.{sha256(entity.encode()).hexdigest()[:16]}.{metric}"
188
+ def add_summary(scope: str, entity: str, values: list[int]) -> None:
189
+ if not values: return
190
+ for metric, value, aggregation in (("average", sum(values)/len(values), "mean"), ("maximum", max(values), "maximum")):
191
+ measurements.append({"measurementId":measurement_id(scope,entity,metric),"capabilityId":COMPLEXITY_CAPABILITY_ID,"metricKey":f"complexity.cyclomatic.{metric}","value":value,"unit":"score","scope":scope,"targetEntityId":entity,"aggregation":aggregation,"sourceAdapterId":COMPLEXITY_ADAPTER_ID,"sourceToolId":"radon"})
192
+ for lower, upper, band in ((1,10,"low"),(11,20,"high"),(21,40,"very_high"),(41,None,"critical")):
193
+ measurements.append({"measurementId":measurement_id(scope,entity,f"distribution.{band}"),"capabilityId":COMPLEXITY_CAPABILITY_ID,"metricKey":"complexity.cyclomatic.distribution","value":sum(1 for value in values if value >= lower and (upper is None or value <= upper)),"unit":"symbols","scope":scope,"targetEntityId":f"{entity}.distribution.{band}","aggregation":"count","sourceAdapterId":COMPLEXITY_ADAPTER_ID,"sourceToolId":"radon"})
194
+ add_summary("repository", context.repository_id, [symbol["complexity"] for symbol in symbols])
195
+ by_language, by_file = {}, {}
196
+ for symbol in symbols:
197
+ by_language.setdefault(symbol["language"], []).append(symbol); by_file.setdefault(symbol["path"], []).append(symbol)
198
+ entity = "symbol." + sha256(f"{symbol['path']}:{symbol['name']}:{symbol['line']}".encode()).hexdigest()[:16]
199
+ evidence = measurement_id("symbol", entity, "value")
200
+ measurements.append({"measurementId":evidence,"capabilityId":COMPLEXITY_CAPABILITY_ID,"metricKey":"complexity.cyclomatic.value","value":symbol["complexity"],"unit":"score","scope":"symbol","targetEntityId":entity,"aggregation":"value","sourceAdapterId":COMPLEXITY_ADAPTER_ID,"sourceToolId":"radon"})
201
+ thresholds = result["thresholds"]
202
+ if symbol["complexity"] >= thresholds["critical"]: rule, severity, title, threshold = "complexity.critical", "CRITICAL", "Critical Complexity", thresholds["critical"]
203
+ elif symbol["complexity"] >= thresholds["veryHigh"]: rule, severity, title, threshold = "complexity.very_high", "HIGH", "Very High Complexity", thresholds["veryHigh"]
204
+ elif symbol["complexity"] >= thresholds["high"]: rule, severity, title, threshold = "complexity.high", "HIGH", "High Complexity", thresholds["high"]
205
+ else: continue
206
+ findings.append({"findingId":f"{rule}.{entity}","capabilityId":COMPLEXITY_CAPABILITY_ID,"ruleId":rule,"severity":severity,"category":"COMPLEXITY","title":title,"description":f"Cyclomatic complexity is {symbol['complexity']} (threshold: {threshold}).","affectedEntityId":entity,"location":{"path":symbol["path"],"line":symbol["line"],"endLine":symbol["endLine"]},"evidenceReferences":[evidence],"state":"OPEN","regression":"UNKNOWN","confidence":1,"suppressible":True})
207
+ for language, values in sorted(by_language.items()): add_summary("language", f"language.{language.lower()}", [item["complexity"] for item in values])
208
+ for path, values in sorted(by_file.items()): add_summary("file", "file."+sha256(path.encode()).hexdigest()[:16], [item["complexity"] for item in values])
209
+ if not symbols:
210
+ findings.append({"findingId":"complexity.missing.repository","capabilityId":COMPLEXITY_CAPABILITY_ID,"ruleId":"complexity.missing","severity":"INFO","category":"COMPLEXITY","title":"Missing Complexity","description":"No supported symbols were measured.","affectedEntityId":context.repository_id,"evidenceReferences":[],"state":"OPEN","regression":"UNKNOWN","confidence":1,"suppressible":False})
211
+ adapter = {"adapter":result["adapter"],"analyzer":result["analyzer"],"execution":"SUCCESS","rawOutputHash":result["rawOutputHash"],"rawOutput":result["rawOutput"],"measuredScope":["repository","language","file","symbol"],"completeness":1,"draftMeasurements":measurements,"draftFindings":findings,"warnings":[],"errors":[],"limitations":result["limitations"],"executionTiming":{"durationMs":duration}}
212
+ capability = {"capabilityId":COMPLEXITY_CAPABILITY_ID,"capabilityVersion":COMPLEXITY_CAPABILITY_VERSION,"status":"VALID","adapterIds":[COMPLEXITY_ADAPTER_ID],"completeness":1,"qualificationApplicable":True,"limitations":result["limitations"],"executionTiming":{"durationMs":duration}}
213
+ return {"measurements":measurements,"findings":findings,"adapterResults":[adapter],"capabilityResults":[capability]}
@@ -0,0 +1,11 @@
1
+ """Derived Maintainability capability; consumes canonical Code Size and Complexity observations only."""
2
+ from __future__ import annotations
3
+ from typing import Any
4
+ CAPABILITY_ID="maintainability"; CAPABILITY_VERSION="0.1.0"
5
+ def derive(code_size: dict[str, Any], complexity: dict[str, Any]) -> dict[str, Any]:
6
+ code=next((m["value"] for m in code_size.get("measurements",[]) if m["metricKey"]=="code_size.code_lines"),None)
7
+ complexity_value=next((m["value"] for m in complexity.get("measurements",[]) if m["metricKey"]=="complexity.cyclomatic.average"),None)
8
+ if code is None or complexity_value is None: return {"status":"BLOCKED","limitations":[{"id":"maintainability.dependencies","description":"Validated Code Size and Complexity evidence is required.","cause":"missing dependency evidence"}]}
9
+ index=max(0.0, min(100.0, 100.0 - complexity_value * 3.0 - (code / 1000.0)))
10
+ metric={"measurementId":"maintainability.repository.index","capabilityId":CAPABILITY_ID,"metricKey":"maintainability.index","value":index,"unit":"index","scope":"repository","targetEntityId":"repository","aggregation":"mean","sourceAdapterId":"derived.maintainability","sourceToolId":"canonical_evidence"}
11
+ return {"status":"VALID","measurements":[metric],"findings":[],"limitations":[]}