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.
- tde_cli/__init__.py +5 -0
- tde_cli/main.py +400 -0
- tde_runtime/__init__.py +6 -0
- tde_runtime/baseline.py +196 -0
- tde_runtime/code_size.py +49 -0
- tde_runtime/complexity.py +92 -0
- tde_runtime/configuration.py +112 -0
- tde_runtime/dependency_health.py +23 -0
- tde_runtime/docker_artifact.py +47 -0
- tde_runtime/evidence_store.py +69 -0
- tde_runtime/execution.py +213 -0
- tde_runtime/maintainability.py +11 -0
- tde_runtime/models.py +70 -0
- tde_runtime/policies/__init__.py +1 -0
- tde_runtime/policies/generation-1.json +18 -0
- tde_runtime/policy.py +202 -0
- tde_runtime/query.py +36 -0
- tde_runtime/registries.py +21 -0
- tde_runtime/release_bundle.py +34 -0
- tde_runtime/release_candidate.py +75 -0
- tde_runtime/release_certification.py +87 -0
- tde_runtime/release_publication.py +114 -0
- tde_runtime/release_qualification.py +115 -0
- tde_runtime/runtime.py +193 -0
- tde_runtime/runtime_qualification.py +63 -0
- tde_runtime/software_assurance.py +183 -0
- tde_runtime/trend.py +73 -0
- tde_runtime/trusted_delivery.py +137 -0
- technical_debt_engine_runtime-0.1.0.dist-info/METADATA +5 -0
- technical_debt_engine_runtime-0.1.0.dist-info/RECORD +33 -0
- technical_debt_engine_runtime-0.1.0.dist-info/WHEEL +5 -0
- technical_debt_engine_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- technical_debt_engine_runtime-0.1.0.dist-info/top_level.txt +2 -0
tde_runtime/code_size.py
ADDED
|
@@ -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"])
|
tde_runtime/execution.py
ADDED
|
@@ -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":[]}
|