technical-debt-engine-runtime 0.2.0__tar.gz → 1.0.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.
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/PKG-INFO +1 -1
- technical_debt_engine_runtime-1.0.0/README.md +44 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/pyproject.toml +2 -2
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/main.py +3 -2
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/code_size.py +2 -1
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/coverage.py +210 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/dependency_health.py +173 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/execution.py +61 -7
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/policies/generation-1.json +18 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policy.py +2 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/profiles/standard.json +13 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/registries.py +5 -1
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime.py +10 -3
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/PKG-INFO +1 -1
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/SOURCES.txt +3 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_cli.py +4 -4
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_code_size.py +8 -1
- technical_debt_engine_runtime-1.0.0/tests/test_coverage.py +118 -0
- technical_debt_engine_runtime-1.0.0/tests/test_dependency_health.py +138 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_policy.py +10 -0
- technical_debt_engine_runtime-1.0.0/tests/test_public_artifacts.py +152 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_public_policy_configuration.py +6 -5
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_bundle.py +2 -2
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_publication.py +7 -7
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_qualification.py +10 -1
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime.py +17 -3
- technical_debt_engine_runtime-0.2.0/README.md +0 -36
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/dependency_health.py +0 -23
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/policies/generation-1.json +0 -16
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/profiles/standard.json +0 -11
- technical_debt_engine_runtime-0.2.0/tests/test_public_artifacts.py +0 -86
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/setup.cfg +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/__init__.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/__init__.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/analyzer_discovery.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/assessment.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/baseline.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/complexity.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/configuration.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/differential.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/evidence_store.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/maintainability.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/models.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policies/__init__.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/profiles/__init__.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/profiles/minimal.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/query.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_bundle.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_certification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_publication.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_qualification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/repository_qualification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime_qualification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/__init__.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/assessment-decision-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/assessment-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/capability-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/common.schema.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/differential-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/policy-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/repository-qualification-evidence.json +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trend.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trusted_delivery.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/dependency_links.txt +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/entry_points.txt +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/requires.txt +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/top_level.txt +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_baseline.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_build_reproducibility.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_capability_platform.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_complexity.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_evidence_store.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_execution.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_query.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_certification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime_qualification.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trend.py +0 -0
- {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trusted_delivery.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Technical Debt Engine
|
|
2
|
+
|
|
3
|
+
DJConnect is the primary product. Technical Debt Engine (TDE) is its compact,
|
|
4
|
+
supporting engineering tool for producing reliable pipeline assessment
|
|
5
|
+
decisions through public evidence and CLI contracts.
|
|
6
|
+
|
|
7
|
+
Generation 2 is the consumer-driven **TDE 1.0 DJConnect Enablement Program**.
|
|
8
|
+
It does not expand TDE into a general platform: its active scope is coverage
|
|
9
|
+
completion, minimal dependency and security evidence, selected DJConnect CI
|
|
10
|
+
consumption, and one integrated `1.0.0` qualification and release. See the
|
|
11
|
+
[roadmap](PRODUCT_ROADMAP.md) and [active backlog](PRODUCT_BACKLOG.md).
|
|
12
|
+
|
|
13
|
+
Release `0.2.0` is available as the PyPI distribution
|
|
14
|
+
[`technical-debt-engine-runtime`](https://pypi.org/project/technical-debt-engine-runtime/0.2.0/),
|
|
15
|
+
a [GitHub Release](https://github.com/pcvantol/technical-debt-engine/releases/tag/0.2.0),
|
|
16
|
+
and Docker image `docker.io/pcvantol/technical-debt-engine:0.2.0`. The Docker
|
|
17
|
+
OCI index is `sha256:8285a5082eaa1a5ac914b349ddec21c9e02cc4269421774d4f112383bc688ca9`;
|
|
18
|
+
no `latest` tag exists. See the [Runtime Qualification Report](RUNTIME_QUALIFICATION_REPORT_0.2.0.md)
|
|
19
|
+
for the immutable publication evidence and public-runtime validation.
|
|
20
|
+
|
|
21
|
+
## Product contracts
|
|
22
|
+
|
|
23
|
+
Consumers integrate only through the public `tde` CLI, configuration, evidence
|
|
24
|
+
schema, exit codes, and stable released contracts—not runtime internals. See
|
|
25
|
+
[INTEGRATION_MODEL.md](INTEGRATION_MODEL.md).
|
|
26
|
+
|
|
27
|
+
Operational repository assurance is available through `tde assure`; see [SOFTWARE_ASSURANCE.md](SOFTWARE_ASSURANCE.md) for its canonical evidence and candidate-artifact verification contract.
|
|
28
|
+
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
- [Product architecture](PRODUCT_ARCHITECTURE.md)
|
|
32
|
+
- [Platform vision](PLATFORM_VISION.md) and [platform strategy](PLATFORM_STRATEGY.md)
|
|
33
|
+
- [Engineering method](ENGINEERING_METHOD.md) and [session bootstrap](BOOTSTRAP.md)
|
|
34
|
+
- [Capability model](CAPABILITY_MODEL.md)
|
|
35
|
+
- [CLI specification](CLI_SPECIFICATION.md)
|
|
36
|
+
- [Code Size Runtime contract](CODE_SIZE_RUNTIME.md)
|
|
37
|
+
- [Evidence schema](EVIDENCE_SCHEMA.md)
|
|
38
|
+
- [Qualification model](QUALIFICATION_MODEL.md)
|
|
39
|
+
- [Roadmap](PRODUCT_ROADMAP.md) and [backlog](PRODUCT_BACKLOG.md)
|
|
40
|
+
- [Versioning](VERSIONING.md) and [release strategy](RELEASE_STRATEGY.md)
|
|
41
|
+
- [Package build reproducibility](PACKAGING.md)
|
|
42
|
+
- [Repository status](REPOSITORY_STATUS.md)
|
|
43
|
+
|
|
44
|
+
The documentation index is [PROMPT_INDEX.md](PROMPT_INDEX.md).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==
|
|
2
|
+
requires = ["setuptools==83.0.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "technical-debt-engine-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Technical Debt Engine runtime foundation API"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = ["radon==6.0.1"]
|
{technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/main.py
RENAMED
|
@@ -29,7 +29,7 @@ from tde_runtime.repository_qualification import (QualificationRegistry,
|
|
|
29
29
|
from tde_runtime.differential import AssessmentBaselineRegistry, DifferentialEngine, DifferentialError
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
CLI_VERSION = "0.
|
|
32
|
+
CLI_VERSION = "1.0.0"
|
|
33
33
|
GENERATION = "1"
|
|
34
34
|
|
|
35
35
|
|
|
@@ -265,7 +265,8 @@ def _apply_command_configuration(configuration: RuntimeConfiguration, arguments:
|
|
|
265
265
|
if arguments.command == "assess":
|
|
266
266
|
return AssessmentOrchestrator().configure(configuration, profile=arguments.profile, capabilities=tuple(item.replace("-", "_") for item in arguments.capability))
|
|
267
267
|
if arguments.command == "release-qualify":
|
|
268
|
-
supported = {"code-size": "code_size", "complexity": "complexity"
|
|
268
|
+
supported = {"code-size": "code_size", "complexity": "complexity", "coverage": "coverage",
|
|
269
|
+
"dependency-health": "dependency_health"}
|
|
269
270
|
if not arguments.release_capability or any(item not in supported for item in arguments.release_capability): raise ValueError("release-qualify requires one or more supported --release-capability values")
|
|
270
271
|
for capability in sorted(set(arguments.release_capability)): configuration = configuration.with_capability(supported[capability])
|
|
271
272
|
return configuration
|
|
@@ -12,6 +12,7 @@ CAPABILITY_VERSION = "0.1.0"
|
|
|
12
12
|
ADAPTER_ID = "code_size.cloc"
|
|
13
13
|
ADAPTER_VERSION = "0.1.0"
|
|
14
14
|
MINIMUM_ANALYZER_VERSION = (2, 10)
|
|
15
|
+
EXCLUDED_DIRECTORIES = (".git", ".tde", "__pycache__", ".venv", "venv", "build", "dist", "bin", "obj", ".build", ".swiftpm", ".pio", ".release", ".public-release")
|
|
15
16
|
|
|
16
17
|
def classify(path: str) -> str:
|
|
17
18
|
value = path.replace("\\", "/").lower()
|
|
@@ -27,7 +28,7 @@ def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
|
|
|
27
28
|
if discovery["status"] != "VALID":
|
|
28
29
|
return {"status": discovery["status"], "limitations": [discovery["limitation"]]}
|
|
29
30
|
try:
|
|
30
|
-
result = subprocess.run([discovery["executable"], "--json", "--by-file", "--quiet", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
|
|
31
|
+
result = subprocess.run([discovery["executable"], "--json", "--by-file", "--quiet", f"--exclude-dir={','.join(EXCLUDED_DIRECTORIES)}", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
|
|
31
32
|
raw = result.stdout
|
|
32
33
|
data = json.loads(raw)
|
|
33
34
|
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Read-only Coverage capability adapters for existing repository artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
from xml.etree import ElementTree
|
|
11
|
+
|
|
12
|
+
CAPABILITY_ID = "coverage"
|
|
13
|
+
CAPABILITY_VERSION = "0.1.0"
|
|
14
|
+
ADAPTER_ID = "coverage.artifact"
|
|
15
|
+
ADAPTER_VERSION = "0.1.0"
|
|
16
|
+
PARSER_VERSION = "1.0.0"
|
|
17
|
+
DEFAULT_LOCATIONS = ("coverage.xml", "coverage/coverage.xml", "reports/coverage.xml", "coverage.json", "coverage/coverage.json", "lcov.info", "coverage/lcov.info", "coverage.info")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CoverageArtifactError(ValueError):
|
|
21
|
+
"""An artifact cannot be trusted as canonical coverage evidence."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _paths(root: Path, settings: Mapping[str, Any]) -> tuple[Path, ...]:
|
|
25
|
+
configured: list[str] = []
|
|
26
|
+
for key in ("path", "artifact", "file"):
|
|
27
|
+
value = settings.get(key)
|
|
28
|
+
if isinstance(value, str) and value.strip():
|
|
29
|
+
configured.append(value)
|
|
30
|
+
values = settings.get("paths")
|
|
31
|
+
if isinstance(values, (list, tuple)) and all(isinstance(value, str) and value.strip() for value in values):
|
|
32
|
+
configured.extend(values)
|
|
33
|
+
candidates = configured or list(DEFAULT_LOCATIONS)
|
|
34
|
+
return tuple((Path(candidate) if Path(candidate).is_absolute() else root / candidate) for candidate in candidates)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _metric(covered: int | None, total: int | None) -> dict[str, int | float | None]:
|
|
38
|
+
if covered is None or total is None:
|
|
39
|
+
return {"covered": None, "total": None, "percent": None}
|
|
40
|
+
if covered < 0 or total < 0 or covered > total:
|
|
41
|
+
raise CoverageArtifactError("coverage totals must be non-negative and covered must not exceed total")
|
|
42
|
+
return {"covered": covered, "total": total, "percent": (covered / total * 100) if total else 100.0}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _integer(value: str | None, label: str) -> int:
|
|
46
|
+
try:
|
|
47
|
+
number = int(value or "")
|
|
48
|
+
except ValueError as error:
|
|
49
|
+
raise CoverageArtifactError(f"{label} must be an integer") from error
|
|
50
|
+
if number < 0:
|
|
51
|
+
raise CoverageArtifactError(f"{label} must not be negative")
|
|
52
|
+
return number
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _xml_metric(root: ElementTree.Element, names: tuple[str, ...], label: str) -> int | None:
|
|
56
|
+
for name in names:
|
|
57
|
+
if name in root.attrib:
|
|
58
|
+
return _integer(root.attrib[name], label)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _rate(root: ElementTree.Element, name: str) -> float | None:
|
|
63
|
+
if name not in root.attrib:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
value = float(root.attrib[name])
|
|
67
|
+
except ValueError as error:
|
|
68
|
+
raise CoverageArtifactError(f"{name} must be numeric") from error
|
|
69
|
+
if not 0 <= value <= 1:
|
|
70
|
+
raise CoverageArtifactError(f"{name} must be between 0 and 1")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_xml(contents: bytes) -> tuple[str, dict[str, Any]]:
|
|
75
|
+
try:
|
|
76
|
+
root = ElementTree.fromstring(contents)
|
|
77
|
+
except ElementTree.ParseError as error:
|
|
78
|
+
raise CoverageArtifactError(f"invalid XML coverage artifact: {error}") from error
|
|
79
|
+
if root.tag.rsplit("}", 1)[-1] != "coverage":
|
|
80
|
+
raise CoverageArtifactError("XML coverage artifact must have a coverage root element")
|
|
81
|
+
# Cobertura class elements own the repository line set. Method elements
|
|
82
|
+
# may repeat those same lines, so a broad ``.//line`` query double-counts
|
|
83
|
+
# reports emitted by Istanbul and gcovr.
|
|
84
|
+
lines = root.findall(".//class/lines/line")
|
|
85
|
+
if not lines:
|
|
86
|
+
raise CoverageArtifactError("XML coverage artifact contains no line data")
|
|
87
|
+
line_hits: list[int] = []
|
|
88
|
+
branches: list[tuple[int, int]] = []
|
|
89
|
+
for line in lines:
|
|
90
|
+
number = line.attrib.get("number")
|
|
91
|
+
hits = _integer(line.attrib.get("hits"), "line hits")
|
|
92
|
+
if not number:
|
|
93
|
+
raise CoverageArtifactError("line coverage record has no line number")
|
|
94
|
+
line_hits.append(hits)
|
|
95
|
+
condition = line.attrib.get("condition-coverage")
|
|
96
|
+
if condition:
|
|
97
|
+
match = re.search(r"\((\d+)\s*/\s*(\d+)\)", condition)
|
|
98
|
+
if not match:
|
|
99
|
+
raise CoverageArtifactError("invalid branch condition coverage")
|
|
100
|
+
branches.append((_integer(match.group(1), "covered branches"), _integer(match.group(2), "total branches")))
|
|
101
|
+
derived_lines = (sum(hits > 0 for hits in line_hits), len(line_hits))
|
|
102
|
+
declared_lines = (_xml_metric(root, ("lines-covered",), "lines covered"), _xml_metric(root, ("lines-valid",), "lines valid"))
|
|
103
|
+
if all(value is not None for value in declared_lines) and declared_lines != derived_lines:
|
|
104
|
+
raise CoverageArtifactError("declared XML line totals disagree with line data")
|
|
105
|
+
derived_branches = (sum(value[0] for value in branches), sum(value[1] for value in branches)) if branches else (None, None)
|
|
106
|
+
declared_branches = (_xml_metric(root, ("branches-covered",), "branches covered"), _xml_metric(root, ("branches-valid",), "branches valid"))
|
|
107
|
+
# Coverage.py emits a zero/zero branch summary when branch collection was
|
|
108
|
+
# not enabled. It is unavailable branch coverage, not 100% coverage.
|
|
109
|
+
if derived_branches[0] is None and declared_branches == (0, 0):
|
|
110
|
+
declared_branches = (None, None)
|
|
111
|
+
# A Cobertura document's root branch summary is the producer's aggregate
|
|
112
|
+
# metric. Some Istanbul reports expose a narrower line-condition view, so
|
|
113
|
+
# it can legitimately differ from the root aggregate. The summary is
|
|
114
|
+
# still self-validated by branch-rate below.
|
|
115
|
+
line = _metric(*(declared_lines if all(value is not None for value in declared_lines) else derived_lines))
|
|
116
|
+
branch = _metric(*(declared_branches if all(value is not None for value in declared_branches) else derived_branches))
|
|
117
|
+
for name, metric in (("line-rate", line), ("branch-rate", branch)):
|
|
118
|
+
rate = _rate(root, name)
|
|
119
|
+
if rate is not None and metric["percent"] is not None and abs(rate * 100 - float(metric["percent"])) > 0.01:
|
|
120
|
+
raise CoverageArtifactError(f"{name} disagrees with coverage totals")
|
|
121
|
+
# coverage.py emits lines-valid and a numeric major version. Other
|
|
122
|
+
# Cobertura producers, including c8 and gcovr, use the same attributes.
|
|
123
|
+
version = root.attrib.get("version", "")
|
|
124
|
+
parser = "coverage.py-xml" if b"generated by coverage.py" in contents.lower() or ("lines-valid" in root.attrib and re.match(r"^[1-9]\d*(?:\.\d+)*$", version)) else "cobertura-xml"
|
|
125
|
+
return parser, {"line": line, "branch": branch}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _parse_xccov_json(contents: bytes) -> tuple[str, dict[str, Any]]:
|
|
129
|
+
"""Normalize the summary emitted by ``xcrun xccov view --json``."""
|
|
130
|
+
try:
|
|
131
|
+
value = json.loads(contents.decode("utf-8"))
|
|
132
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
133
|
+
raise CoverageArtifactError(f"invalid xccov JSON artifact: {error}") from error
|
|
134
|
+
if not isinstance(value, dict):
|
|
135
|
+
raise CoverageArtifactError("xccov JSON artifact must be an object")
|
|
136
|
+
covered = value.get("coveredLines")
|
|
137
|
+
total = value.get("executableLines")
|
|
138
|
+
if not isinstance(covered, int) or not isinstance(total, int):
|
|
139
|
+
raise CoverageArtifactError("xccov JSON requires integer coveredLines and executableLines")
|
|
140
|
+
line = _metric(covered, total)
|
|
141
|
+
declared_rate = value.get("lineCoverage")
|
|
142
|
+
if not isinstance(declared_rate, (int, float)) or not 0 <= float(declared_rate) <= 1:
|
|
143
|
+
raise CoverageArtifactError("xccov JSON lineCoverage must be a ratio between 0 and 1")
|
|
144
|
+
if line["percent"] is not None and abs(float(declared_rate) * 100 - float(line["percent"])) > 0.01:
|
|
145
|
+
raise CoverageArtifactError("xccov JSON lineCoverage disagrees with line totals")
|
|
146
|
+
return "xccov-json", {"line": line, "branch": _metric(None, None)}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _parse_lcov(contents: bytes) -> tuple[str, dict[str, Any]]:
|
|
150
|
+
try:
|
|
151
|
+
rows = contents.decode("utf-8").splitlines()
|
|
152
|
+
except UnicodeDecodeError as error:
|
|
153
|
+
raise CoverageArtifactError("LCOV artifact is not UTF-8") from error
|
|
154
|
+
lines: dict[tuple[str, str], int] = {}
|
|
155
|
+
branches: dict[tuple[str, str, str, str], int] = {}
|
|
156
|
+
source: str | None = None
|
|
157
|
+
for raw in rows:
|
|
158
|
+
row = raw.strip()
|
|
159
|
+
if not row or row == "end_of_record":
|
|
160
|
+
continue
|
|
161
|
+
if row.startswith("SF:"):
|
|
162
|
+
source = row[3:] or None
|
|
163
|
+
if source is None:
|
|
164
|
+
raise CoverageArtifactError("LCOV source file is empty")
|
|
165
|
+
elif row.startswith("DA:"):
|
|
166
|
+
if source is None:
|
|
167
|
+
raise CoverageArtifactError("LCOV line record precedes source file")
|
|
168
|
+
parts = row[3:].split(",")
|
|
169
|
+
if len(parts) < 2:
|
|
170
|
+
raise CoverageArtifactError("invalid LCOV line record")
|
|
171
|
+
key = (source, parts[0]); hits = _integer(parts[1], "LCOV line hits")
|
|
172
|
+
if key in lines and lines[key] != hits:
|
|
173
|
+
raise CoverageArtifactError("duplicate LCOV line records disagree")
|
|
174
|
+
lines[key] = hits
|
|
175
|
+
elif row.startswith("BRDA:"):
|
|
176
|
+
if source is None:
|
|
177
|
+
raise CoverageArtifactError("LCOV branch record precedes source file")
|
|
178
|
+
parts = row[5:].split(",")
|
|
179
|
+
if len(parts) != 4:
|
|
180
|
+
raise CoverageArtifactError("invalid LCOV branch record")
|
|
181
|
+
key = (source, *parts[:3]); taken = 0 if parts[3] == "-" else _integer(parts[3], "LCOV branch hits")
|
|
182
|
+
if key in branches and branches[key] != taken:
|
|
183
|
+
raise CoverageArtifactError("duplicate LCOV branch records disagree")
|
|
184
|
+
branches[key] = taken
|
|
185
|
+
if not lines:
|
|
186
|
+
raise CoverageArtifactError("LCOV artifact contains no line data")
|
|
187
|
+
return "lcov", {"line": _metric(sum(value > 0 for value in lines.values()), len(lines)),
|
|
188
|
+
"branch": _metric(sum(value > 0 for value in branches.values()), len(branches)) if branches else _metric(None, None)}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def analyze(root: Path, settings: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
192
|
+
"""Discover, validate, and normalize one pre-existing coverage artifact."""
|
|
193
|
+
settings = settings or {}
|
|
194
|
+
artifact = next((path for path in _paths(root, settings) if path.is_file()), None)
|
|
195
|
+
if artifact is None:
|
|
196
|
+
return {"status": "VALID", "available": False, "limitations": [{"id": "coverage.artifact.unavailable", "description": "No coverage artifact was found in configured or standard locations.", "cause": "artifact unavailable"}]}
|
|
197
|
+
try:
|
|
198
|
+
contents = artifact.read_bytes()
|
|
199
|
+
if artifact.suffix.lower() in {".info", ".lcov"} or artifact.name == "lcov.info":
|
|
200
|
+
parser, metrics = _parse_lcov(contents)
|
|
201
|
+
elif artifact.suffix.lower() == ".json":
|
|
202
|
+
parser, metrics = _parse_xccov_json(contents)
|
|
203
|
+
else:
|
|
204
|
+
parser, metrics = _parse_xml(contents)
|
|
205
|
+
except (OSError, CoverageArtifactError) as error:
|
|
206
|
+
return {"status": "FAILED_CLOSED", "limitations": [{"id": "coverage.artifact.invalid", "description": str(error), "cause": "invalid coverage artifact", "blocking": True}]}
|
|
207
|
+
return {"status": "VALID", "available": True, "parser": parser, "parserVersion": PARSER_VERSION,
|
|
208
|
+
"sourceFormat": "lcov" if parser == "lcov" else "json" if parser == "xccov-json" else "xml", "artifact": artifact.relative_to(root).as_posix() if artifact.is_relative_to(root) else artifact.name,
|
|
209
|
+
"rawOutput": contents.decode("utf-8", errors="replace"), "rawOutputHash": "sha256:" + sha256(contents).hexdigest(),
|
|
210
|
+
"metrics": metrics, "limitations": []}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Consumer-driven dependency-health normalization for DJConnect repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import tomllib
|
|
14
|
+
from typing import Any
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ADAPTER_ID = "dependency_health.platform"
|
|
19
|
+
ADAPTER_VERSION = "1.0.0"
|
|
20
|
+
CAPABILITY_ID = "dependency_health"
|
|
21
|
+
CAPABILITY_VERSION = "1.0.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def analyze(root: Path, timeout: int) -> dict[str, Any]:
|
|
25
|
+
"""Normalize only package ecosystems actually present in a repository."""
|
|
26
|
+
records = []
|
|
27
|
+
if (root / "package.json").is_file(): records.append(_npm(root, timeout))
|
|
28
|
+
python = _python(root, timeout)
|
|
29
|
+
if python is not None: records.append(python)
|
|
30
|
+
nuget = _nuget(root, timeout)
|
|
31
|
+
if nuget is not None: records.append(nuget)
|
|
32
|
+
if (root / "platformio.ini").is_file(): records.append(_platformio(root, timeout))
|
|
33
|
+
if (root / "Package.swift").is_file(): records.append(_swift(root, timeout))
|
|
34
|
+
if not records:
|
|
35
|
+
return _unavailable("no supported dependency manifest was found")
|
|
36
|
+
limitations = [item for record in records for item in record["limitations"]]
|
|
37
|
+
if any(item["blocking"] for item in limitations):
|
|
38
|
+
return {"status": "BLOCKED", "limitations": limitations}
|
|
39
|
+
raw = "\n".join(item["rawOutput"] for item in records)
|
|
40
|
+
return {"status": "VALID", "ecosystems": records, "rawOutput": raw,
|
|
41
|
+
"rawOutputHash": "sha256:" + sha256(raw.encode()).hexdigest(),
|
|
42
|
+
"limitations": limitations}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _npm(root: Path, timeout: int) -> dict[str, Any]:
|
|
46
|
+
package, lock = root / "package.json", root / "package-lock.json"
|
|
47
|
+
try:
|
|
48
|
+
manifest = json.loads(package.read_text(encoding="utf-8"))
|
|
49
|
+
lock_data = json.loads(lock.read_text(encoding="utf-8")) if lock.is_file() else {}
|
|
50
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
51
|
+
return _blocked("npm", "npm", str(error))
|
|
52
|
+
direct = _npm_direct(manifest); packages = lock_data.get("packages", {})
|
|
53
|
+
if not isinstance(packages, dict): packages = {}
|
|
54
|
+
resolved = sorted(path.removeprefix("node_modules/") for path in packages if path.startswith("node_modules/"))
|
|
55
|
+
unknown = sorted(name for name in direct if lock.is_file() and f"node_modules/{name}" not in packages)
|
|
56
|
+
outdated, analyzer, raw, limitations = _outdated_command("npm", ["npm", "outdated", "--json", "--package-lock-only"], root, timeout)
|
|
57
|
+
return _record("npm", "npm", direct, [name for name in resolved if name not in direct], unknown, outdated, analyzer, raw, limitations)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _python(root: Path, timeout: int) -> dict[str, Any] | None:
|
|
61
|
+
requirements = _manifest_files(root, "requirements*.txt")
|
|
62
|
+
pyproject = root / "pyproject.toml"
|
|
63
|
+
if not requirements and not pyproject.is_file(): return None
|
|
64
|
+
direct: dict[str, str] = {}
|
|
65
|
+
for path in requirements:
|
|
66
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
67
|
+
match = re.match(r"^\s*([A-Za-z0-9_.-]+)\s*(.*)$", line)
|
|
68
|
+
if match and not line.lstrip().startswith(("#", "-")): direct[match.group(1)] = match.group(2).strip()
|
|
69
|
+
if pyproject.is_file():
|
|
70
|
+
try:
|
|
71
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
72
|
+
for requirement in data.get("project", {}).get("dependencies", []):
|
|
73
|
+
match = re.match(r"^\s*([A-Za-z0-9_.-]+)\s*(.*)$", requirement)
|
|
74
|
+
if match: direct[match.group(1)] = match.group(2).strip()
|
|
75
|
+
except (OSError, tomllib.TOMLDecodeError) as error: return _blocked("PyPI", "pip", str(error))
|
|
76
|
+
pip = shutil.which("pip") or shutil.which("pip3")
|
|
77
|
+
if not pip: return _record("PyPI", "pip", sorted(direct), None, None, None, {"id": "pip", "version": "UNAVAILABLE"}, "", [_limitation("dependency_health.pip.unavailable", "pip was not found on PATH")])
|
|
78
|
+
version = _version(pip, root, timeout); outdated, raw, limitations = [], [], []
|
|
79
|
+
for name, constraint in sorted(direct.items()):
|
|
80
|
+
if not constraint.startswith("=="):
|
|
81
|
+
outdated = None; limitations.append(_limitation("dependency_health.python.outdated.unavailable", f"{name} is not pinned")); continue
|
|
82
|
+
try:
|
|
83
|
+
completed = subprocess.run([pip, "index", "versions", name], cwd=root, capture_output=True, text=True, timeout=timeout, check=True)
|
|
84
|
+
raw.append(completed.stdout); latest = re.search(r"Available versions:\s*([^,\s]+)", completed.stdout)
|
|
85
|
+
if latest and latest.group(1) != constraint[2:]: outdated.append(name)
|
|
86
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
87
|
+
outdated = None; limitations.append(_limitation("dependency_health.python.outdated.unavailable", str(error)))
|
|
88
|
+
return _record("PyPI", "pip", sorted(direct), None, None, outdated, {"id": "pip", "version": version}, "".join(raw), limitations)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _nuget(root: Path, timeout: int) -> dict[str, Any] | None:
|
|
92
|
+
projects = _manifest_files(root, "*.csproj")
|
|
93
|
+
if not projects: return None
|
|
94
|
+
direct = []
|
|
95
|
+
for project in projects:
|
|
96
|
+
try: direct.extend(item.attrib["Include"] for item in ET.parse(project).iter() if item.tag.endswith("PackageReference") and "Include" in item.attrib)
|
|
97
|
+
except (ET.ParseError, OSError) as error: return _blocked("NuGet", "dotnet", str(error))
|
|
98
|
+
dotnet = shutil.which("dotnet")
|
|
99
|
+
if not dotnet: return _record("NuGet", "dotnet", sorted(set(direct)), None, None, None, {"id": "dotnet", "version": "UNAVAILABLE"}, "", [_limitation("dependency_health.dotnet.unavailable", "dotnet was not found on PATH")])
|
|
100
|
+
raw, outdated, transitive, limitations = [], [], [], []
|
|
101
|
+
for project in projects:
|
|
102
|
+
try:
|
|
103
|
+
completed = subprocess.run([dotnet, "package", "list", "--project", str(project), "--outdated", "--include-transitive", "--format", "json"], cwd=root, capture_output=True, text=True, timeout=timeout, check=False)
|
|
104
|
+
raw.append(completed.stdout); payload = json.loads(completed.stdout or "{}")
|
|
105
|
+
problems = payload.get("problems", [])
|
|
106
|
+
errors = [item.get("text", "NuGet analysis failed") for item in problems if item.get("level") == "error"]
|
|
107
|
+
if completed.returncode != 0 or errors:
|
|
108
|
+
detail = "; ".join(errors) or completed.stderr.strip() or f"dotnet exited with status {completed.returncode}"
|
|
109
|
+
return _analysis_failed("NuGet", "dotnet", f"{project}: {detail}")
|
|
110
|
+
for project_data in payload.get("projects", []):
|
|
111
|
+
for framework in project_data.get("frameworks", []):
|
|
112
|
+
outdated.extend(item["id"] for item in framework.get("topLevelPackages", []) if item.get("latestVersion"))
|
|
113
|
+
outdated.extend(item["id"] for item in framework.get("transitivePackages", []) if item.get("latestVersion"))
|
|
114
|
+
transitive.extend(item["id"] for item in framework.get("transitivePackages", []) if item.get("id"))
|
|
115
|
+
except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as error:
|
|
116
|
+
outdated = None; transitive = None; limitations.append(_limitation("dependency_health.nuget.outdated.unavailable", str(error)))
|
|
117
|
+
return _record("NuGet", "dotnet", sorted(set(direct)), None if transitive is None else sorted(set(transitive) - set(direct)), None, None if outdated is None else sorted(set(outdated)), {"id": "dotnet", "version": _version(dotnet, root, timeout)}, "".join(raw), limitations)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _platformio(root: Path, timeout: int) -> dict[str, Any]:
|
|
121
|
+
direct = []
|
|
122
|
+
lines = (root / "platformio.ini").read_text(encoding="utf-8").splitlines(); in_deps = False
|
|
123
|
+
for line in lines:
|
|
124
|
+
if line.strip().startswith("lib_deps"): in_deps = True; continue
|
|
125
|
+
if in_deps and line and not line[0].isspace(): in_deps = False
|
|
126
|
+
if in_deps and line.strip(): direct.append(line.strip().split("@")[0])
|
|
127
|
+
pio = shutil.which("pio")
|
|
128
|
+
if not pio: return _record("PlatformIO", "PlatformIO", direct, None, None, None, {"id": "pio", "version": "UNAVAILABLE"}, "", [_limitation("dependency_health.platformio.unavailable", "pio was not found on PATH")])
|
|
129
|
+
try:
|
|
130
|
+
completed = subprocess.run([pio, "pkg", "outdated", "-d", str(root)], cwd=root, capture_output=True, text=True, timeout=timeout, check=False)
|
|
131
|
+
raw = completed.stdout + completed.stderr
|
|
132
|
+
outdated = [line.split()[0] for line in raw.splitlines() if re.match(r"^[A-Za-z0-9_.-]+\s+\d", line) and len(line.split()) >= 4 and line.split()[1] != line.split()[3]]
|
|
133
|
+
return _record("PlatformIO", "PlatformIO", sorted(direct), None, None, sorted(set(outdated)), {"id": "pio", "version": _version(pio, root, timeout)}, raw, [])
|
|
134
|
+
except (OSError, subprocess.SubprocessError) as error: return _record("PlatformIO", "PlatformIO", sorted(direct), None, None, None, {"id": "pio", "version": "UNAVAILABLE"}, "", [_limitation("dependency_health.platformio.outdated.unavailable", str(error))])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _swift(root: Path, timeout: int) -> dict[str, Any]:
|
|
138
|
+
contents = (root / "Package.swift").read_text(encoding="utf-8")
|
|
139
|
+
direct = re.findall(r"\.package\([^\n]*?(?:url|path):\s*\"([^\"]+)", contents)
|
|
140
|
+
analyzer = {"id": "swift", "version": "NOT_REQUIRED"}
|
|
141
|
+
limitations = [] if not direct else [_limitation("dependency_health.swift.outdated.unavailable", "SwiftPM has no non-mutating outdated command")]
|
|
142
|
+
return _record("SwiftPM", "SwiftPM", sorted(direct), None, None, [] if not direct else None, analyzer, "", limitations)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _record(ecosystem: str, manager: str, direct: list[str], transitive: list[str] | None, unknown: list[str] | None, outdated: list[str] | None, analyzer: dict[str, str], raw: str, limitations: list[dict[str, Any]]) -> dict[str, Any]:
|
|
146
|
+
return {"ecosystem": ecosystem, "packageManager": manager, "directDependencies": direct, "transitiveDependencies": transitive, "unknownDependencies": unknown, "outdatedDependencies": outdated, "analyzer": analyzer, "rawOutput": raw, "limitations": limitations}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _blocked(ecosystem: str, manager: str, reason: str) -> dict[str, Any]: return _record(ecosystem, manager, [], None, None, None, {"id": manager, "version": "UNAVAILABLE"}, "", [_limitation("dependency_health.manifest.invalid", reason, True)])
|
|
150
|
+
def _analysis_failed(ecosystem: str, manager: str, reason: str) -> dict[str, Any]: return _record(ecosystem, manager, [], None, None, None, {"id": manager, "version": "UNAVAILABLE"}, "", [_limitation(f"dependency_health.{manager}.analysis.failed", reason, True)])
|
|
151
|
+
def _unavailable(reason: str) -> dict[str, Any]: return {"status": "VALID", "ecosystems": [], "rawOutput": "", "rawOutputHash": "sha256:" + sha256(b"").hexdigest(), "limitations": [_limitation("dependency_health.ecosystem.unavailable", reason)] , "available": False}
|
|
152
|
+
def _limitation(identifier: str, description: str, blocking: bool = False) -> dict[str, Any]: return {"id": identifier, "description": description, "cause": "dependency health", "blocking": blocking}
|
|
153
|
+
def _version(executable: str, root: Path, timeout: int) -> str:
|
|
154
|
+
try: return subprocess.run([executable, "--version"], cwd=root, capture_output=True, text=True, timeout=timeout, check=True).stdout.strip().splitlines()[0]
|
|
155
|
+
except (OSError, subprocess.SubprocessError): return "UNAVAILABLE"
|
|
156
|
+
def _npm_direct(manifest: dict[str, Any]) -> list[str]: return sorted({name for key in ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies") for name in manifest.get(key, {}) if isinstance(manifest.get(key), dict)})
|
|
157
|
+
def _outdated_command(name: str, command: list[str], root: Path, timeout: int) -> tuple[list[str] | None, dict[str, str], str, list[dict[str, Any]]]:
|
|
158
|
+
executable = shutil.which(name)
|
|
159
|
+
if not executable: return None, {"id": name, "version": "UNAVAILABLE"}, "", [_limitation(f"dependency_health.{name}.unavailable", f"{name} was not found on PATH")]
|
|
160
|
+
try:
|
|
161
|
+
completed = subprocess.run([executable, *command[1:]], cwd=root, capture_output=True, text=True, timeout=timeout, check=False); raw = completed.stdout or "{}"; parsed = json.loads(raw)
|
|
162
|
+
return sorted(parsed) if isinstance(parsed, dict) else None, {"id": name, "version": _version(executable, root, timeout)}, raw, []
|
|
163
|
+
except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as error: return None, {"id": name, "version": "UNAVAILABLE"}, "", [_limitation(f"dependency_health.{name}.outdated.unavailable", str(error))]
|
|
164
|
+
def _manifest_files(root: Path, pattern: str) -> list[Path]:
|
|
165
|
+
manifests = []
|
|
166
|
+
for directory, names, files in os.walk(root):
|
|
167
|
+
names[:] = [name for name in names if not _ignored_directory(name)]
|
|
168
|
+
manifests.extend(Path(directory, name) for name in files if fnmatch.fnmatch(name, pattern))
|
|
169
|
+
return sorted(manifests)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _ignored_directory(name: str) -> bool:
|
|
173
|
+
return name in {".git", "node_modules", "dist", "build", ".venv", "venv", ".build", ".swiftpm", ".pio", ".release", ".public-release"} or name.startswith(".xcode-derived")
|
|
@@ -8,7 +8,8 @@ from hashlib import sha256
|
|
|
8
8
|
|
|
9
9
|
from .code_size import ADAPTER_ID, CAPABILITY_ID, CAPABILITY_VERSION, analyze
|
|
10
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 .
|
|
11
|
+
from .coverage import ADAPTER_ID as COVERAGE_ADAPTER_ID, CAPABILITY_ID as COVERAGE_CAPABILITY_ID, CAPABILITY_VERSION as COVERAGE_CAPABILITY_VERSION, analyze as analyze_coverage
|
|
12
|
+
from .dependency_health import ADAPTER_ID as DEPENDENCY_ADAPTER_ID, CAPABILITY_ID as DEPENDENCY_CAPABILITY_ID, CAPABILITY_VERSION as DEPENDENCY_CAPABILITY_VERSION, analyze as analyze_dependencies
|
|
12
13
|
from .maintainability import CAPABILITY_ID as MAINTAINABILITY_CAPABILITY_ID, CAPABILITY_VERSION as MAINTAINABILITY_CAPABILITY_VERSION, derive as derive_maintainability
|
|
13
14
|
from .registries import AdapterRegistry, CapabilityRegistry
|
|
14
15
|
|
|
@@ -112,13 +113,19 @@ class CapabilityExecutionEngine:
|
|
|
112
113
|
if result["status"] != "VALID":
|
|
113
114
|
return self._blocked(COMPLEXITY_CAPABILITY_ID, COMPLEXITY_CAPABILITY_VERSION, [COMPLEXITY_ADAPTER_ID], result["limitations"], duration)
|
|
114
115
|
return self._complexity_result(context, result, duration)
|
|
115
|
-
if identifier ==
|
|
116
|
-
|
|
116
|
+
if identifier == COVERAGE_CAPABILITY_ID and selected_adapter == COVERAGE_ADAPTER_ID:
|
|
117
|
+
settings = context.execution_options.get("capabilities", {}).get(COVERAGE_CAPABILITY_ID, {})
|
|
118
|
+
result = analyze_coverage(context.repository_root, settings)
|
|
117
119
|
duration = int((perf_counter() - started) * 1000)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
if result["status"] != "VALID":
|
|
121
|
+
return self._blocked(COVERAGE_CAPABILITY_ID, COVERAGE_CAPABILITY_VERSION, [COVERAGE_ADAPTER_ID], result["limitations"], duration, result["status"])
|
|
122
|
+
return self._coverage_result(context, result, duration)
|
|
123
|
+
if identifier == DEPENDENCY_CAPABILITY_ID and selected_adapter == DEPENDENCY_ADAPTER_ID:
|
|
124
|
+
result = analyze_dependencies(context.repository_root, timeout)
|
|
125
|
+
duration = int((perf_counter() - started) * 1000)
|
|
126
|
+
if result["status"] != "VALID":
|
|
127
|
+
return self._blocked(DEPENDENCY_CAPABILITY_ID, DEPENDENCY_CAPABILITY_VERSION, [DEPENDENCY_ADAPTER_ID], result["limitations"], duration)
|
|
128
|
+
return self._dependency_result(context, result, duration)
|
|
122
129
|
if identifier != MAINTAINABILITY_CAPABILITY_ID:
|
|
123
130
|
return self._blocked(identifier, "0.1.0", [], [{"id": "adapter.selection.unavailable", "description": f"no selected adapter can execute {identifier}", "cause": "adapter selection"}], 0)
|
|
124
131
|
code = {"measurements": measurements}
|
|
@@ -231,3 +238,50 @@ class CapabilityExecutionEngine:
|
|
|
231
238
|
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}}
|
|
232
239
|
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}}
|
|
233
240
|
return {"measurements":measurements,"findings":findings,"adapterResults":[adapter],"capabilityResults":[capability]}
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _coverage_result(context: Any, result: dict[str, Any], duration: int) -> dict[str, Any]:
|
|
244
|
+
measurements = []
|
|
245
|
+
for kind, label, coverage_key in (("line", "lines", "line_coverage"), ("branch", "branches", "branch_coverage")):
|
|
246
|
+
values = result.get("metrics", {}).get(kind, {"covered": None, "total": None, "percent": None})
|
|
247
|
+
for field, suffix, unit, aggregation in (("percent", coverage_key, "percent", "ratio"), ("covered", f"covered_{label}", label, "sum"), ("total", f"total_{label}", label, "sum")):
|
|
248
|
+
measurements.append({"measurementId": f"coverage.repository.{suffix}", "capabilityId": COVERAGE_CAPABILITY_ID,
|
|
249
|
+
"metricKey": f"coverage.{suffix}", "value": values[field], "availability": "AVAILABLE" if values[field] is not None else "UNAVAILABLE",
|
|
250
|
+
"unit": unit, "scope": "repository", "targetEntityId": context.repository_id, "aggregation": aggregation,
|
|
251
|
+
"sourceAdapterId": COVERAGE_ADAPTER_ID, "sourceToolId": result.get("parser", "coverage-artifact")})
|
|
252
|
+
adapter = {"adapter": {"id": COVERAGE_ADAPTER_ID, "version": "0.1.0"},
|
|
253
|
+
"analyzer": {"id": result.get("parser", "coverage-artifact"), "version": result.get("parserVersion", "1.0.0")},
|
|
254
|
+
"execution": "SUCCESS", "rawOutputHash": result.get("rawOutputHash"), "rawOutput": result.get("rawOutput"),
|
|
255
|
+
"measuredScope": ["repository"], "completeness": 1,
|
|
256
|
+
"draftMeasurements": measurements, "draftFindings": [], "warnings": [], "errors": [], "limitations": result["limitations"],
|
|
257
|
+
"evidence": {"parser": result.get("parser"), "parserVersion": result.get("parserVersion"), "sourceFormat": result.get("sourceFormat"), "artifact": result.get("artifact"), "metrics": result.get("metrics")},
|
|
258
|
+
"executionTiming": {"durationMs": duration}}
|
|
259
|
+
capability = {"capabilityId": COVERAGE_CAPABILITY_ID, "capabilityVersion": COVERAGE_CAPABILITY_VERSION, "status": "VALID",
|
|
260
|
+
"adapterIds": [COVERAGE_ADAPTER_ID], "completeness": 1,
|
|
261
|
+
"qualificationApplicable": bool(result["available"]), "limitations": result["limitations"], "executionTiming": {"durationMs": duration}}
|
|
262
|
+
return {"measurements": measurements, "findings": [], "adapterResults": [adapter], "capabilityResults": [capability]}
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _dependency_result(context: Any, result: dict[str, Any], duration: int) -> dict[str, Any]:
|
|
266
|
+
measurements = []
|
|
267
|
+
records = result["ecosystems"] or [{"ecosystem": "unavailable", "directDependencies": None, "transitiveDependencies": None,
|
|
268
|
+
"unknownDependencies": None, "outdatedDependencies": None,
|
|
269
|
+
"analyzer": {"id": "unavailable", "version": "UNAVAILABLE"}}]
|
|
270
|
+
for record in records:
|
|
271
|
+
values = (("dependency_count", None if record["directDependencies"] is None or record["transitiveDependencies"] is None else record["directDependencies"] + record["transitiveDependencies"]),
|
|
272
|
+
("direct_dependencies", record["directDependencies"]), ("transitive_dependencies", record["transitiveDependencies"]),
|
|
273
|
+
("unknown_dependencies", record["unknownDependencies"]), ("outdated_dependencies", record["outdatedDependencies"]))
|
|
274
|
+
ecosystem = record["ecosystem"].lower().replace(" ", "_")
|
|
275
|
+
measurements.extend({"measurementId": f"dependency_health.{ecosystem}.{key}", "capabilityId": DEPENDENCY_CAPABILITY_ID,
|
|
276
|
+
"metricKey": f"dependency_health.{key}", "value": None if value is None else len(value),
|
|
277
|
+
"availability": "UNAVAILABLE" if value is None else "AVAILABLE", "unit": "packages", "scope": "ecosystem",
|
|
278
|
+
"targetEntityId": f"ecosystem.{ecosystem}", "aggregation": "count", "sourceAdapterId": DEPENDENCY_ADAPTER_ID,
|
|
279
|
+
"sourceToolId": record["analyzer"]["id"]} for key, value in values)
|
|
280
|
+
adapter = {"adapter": {"id": DEPENDENCY_ADAPTER_ID, "version": "1.0.0"}, "analyzer": {"id": "platform", "version": "1.0.0"}, "execution": "SUCCESS",
|
|
281
|
+
"rawOutputHash": result["rawOutputHash"], "rawOutput": result["rawOutput"], "measuredScope": ["repository"], "completeness": 1,
|
|
282
|
+
"draftMeasurements": measurements, "draftFindings": [], "warnings": [], "errors": [], "limitations": result["limitations"],
|
|
283
|
+
"evidence": {"ecosystems": result["ecosystems"]}, "executionTiming": {"durationMs": duration}}
|
|
284
|
+
capability = {"capabilityId": DEPENDENCY_CAPABILITY_ID, "capabilityVersion": DEPENDENCY_CAPABILITY_VERSION, "status": "VALID",
|
|
285
|
+
"adapterIds": [DEPENDENCY_ADAPTER_ID], "completeness": 1, "qualificationApplicable": bool(result.get("available", True)),
|
|
286
|
+
"limitations": result["limitations"], "executionTiming": {"durationMs": duration}}
|
|
287
|
+
return {"measurements": measurements, "findings": [], "adapterResults": [adapter], "capabilityResults": [capability]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"identifier": "tde.generation-1.default",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"scope": "repository",
|
|
5
|
+
"owner": "TDE policy governance",
|
|
6
|
+
"description": "Default, language-independent Generation 1 qualification policy.",
|
|
7
|
+
"supportedCapabilities": ["code_size", "complexity", "coverage", "dependency_health"],
|
|
8
|
+
"supportedSchemas": ["1.0.0"],
|
|
9
|
+
"supportedRuntimeVersions": ["0.2.0", "1.0.0rc1", "1.0.0rc2", "1.0.0rc3", "1.0.0"],
|
|
10
|
+
"rules": [
|
|
11
|
+
{"id": "code_size.repository_lines", "type": "threshold", "capability": "code_size", "metric": "code_size.code_lines", "operator": "greater_than", "threshold": {"warning": 25000, "blocking": 50000}, "severity": {"warning": "WARNING", "blocking": "BLOCKING"}, "enabled": true, "rationale": "Large repositories require deliberate decomposition without treating established product repositories as a blocking condition."},
|
|
12
|
+
{"id": "complexity.maximum", "type": "threshold", "capability": "complexity", "metric": "complexity.cyclomatic.maximum", "operator": "greater_than", "threshold": {"warning": 15, "blocking": 30}, "severity": {"warning": "WARNING", "blocking": "BLOCKING"}, "enabled": true, "rationale": "High cyclomatic complexity impedes safe change; the blocking threshold accommodates the current measured maximum while retaining a warning signal."},
|
|
13
|
+
{"id": "dependency_health.unknown", "type": "threshold", "capability": "dependency_health", "metric": "dependency_health.unknown_dependencies", "operator": "greater_than", "threshold": {"warning": 1, "blocking": 5}, "severity": {"warning": "WARNING", "blocking": "BLOCKING"}, "enabled": true, "rationale": "Unresolved direct dependencies make pipeline dependency evidence unreliable."},
|
|
14
|
+
{"id": "dependency_health.outdated", "type": "threshold", "capability": "dependency_health", "metric": "dependency_health.outdated_dependencies", "operator": "greater_than", "threshold": {"warning": 5, "blocking": 20}, "severity": {"warning": "WARNING", "blocking": "BLOCKING"}, "enabled": true, "rationale": "A bounded number of outdated dependencies keeps review work visible without prescribing updates."},
|
|
15
|
+
{"id": "critical.finding", "type": "finding_severity", "severity": "CRITICAL", "outcome": "FAIL"},
|
|
16
|
+
{"id": "comparison.regression", "type": "comparison_regression", "outcome": "FAIL"}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -14,6 +14,8 @@ POLICY_DECISIONS = ("PASS", "PASS_WITH_WARNINGS", "FAIL", "BLOCKED", "NOT_APPLIC
|
|
|
14
14
|
SUPPORTED_POLICY_METRICS = {
|
|
15
15
|
"code_size": {"code_size.code_lines"},
|
|
16
16
|
"complexity": {"complexity.cyclomatic.maximum"},
|
|
17
|
+
"coverage": {"coverage.line_coverage", "coverage.branch_coverage"},
|
|
18
|
+
"dependency_health": {"dependency_health.unknown_dependencies", "dependency_health.outdated_dependencies"},
|
|
17
19
|
}
|
|
18
20
|
POLICY_OPERATORS = {"greater_than", "less_than"}
|
|
19
21
|
|