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.
Files changed (84) hide show
  1. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/PKG-INFO +1 -1
  2. technical_debt_engine_runtime-1.0.0/README.md +44 -0
  3. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/pyproject.toml +2 -2
  4. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/main.py +3 -2
  5. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/code_size.py +2 -1
  6. technical_debt_engine_runtime-1.0.0/src/tde_runtime/coverage.py +210 -0
  7. technical_debt_engine_runtime-1.0.0/src/tde_runtime/dependency_health.py +173 -0
  8. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/execution.py +61 -7
  9. technical_debt_engine_runtime-1.0.0/src/tde_runtime/policies/generation-1.json +18 -0
  10. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policy.py +2 -0
  11. technical_debt_engine_runtime-1.0.0/src/tde_runtime/profiles/standard.json +13 -0
  12. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/registries.py +5 -1
  13. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime.py +10 -3
  14. {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
  15. {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
  16. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_cli.py +4 -4
  17. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_code_size.py +8 -1
  18. technical_debt_engine_runtime-1.0.0/tests/test_coverage.py +118 -0
  19. technical_debt_engine_runtime-1.0.0/tests/test_dependency_health.py +138 -0
  20. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_policy.py +10 -0
  21. technical_debt_engine_runtime-1.0.0/tests/test_public_artifacts.py +152 -0
  22. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_public_policy_configuration.py +6 -5
  23. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_bundle.py +2 -2
  24. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_publication.py +7 -7
  25. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_qualification.py +10 -1
  26. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime.py +17 -3
  27. technical_debt_engine_runtime-0.2.0/README.md +0 -36
  28. technical_debt_engine_runtime-0.2.0/src/tde_runtime/dependency_health.py +0 -23
  29. technical_debt_engine_runtime-0.2.0/src/tde_runtime/policies/generation-1.json +0 -16
  30. technical_debt_engine_runtime-0.2.0/src/tde_runtime/profiles/standard.json +0 -11
  31. technical_debt_engine_runtime-0.2.0/tests/test_public_artifacts.py +0 -86
  32. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/setup.cfg +0 -0
  33. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/__init__.py +0 -0
  34. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/__init__.py +0 -0
  35. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/analyzer_discovery.py +0 -0
  36. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/assessment.py +0 -0
  37. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/baseline.py +0 -0
  38. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/complexity.py +0 -0
  39. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/configuration.py +0 -0
  40. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/differential.py +0 -0
  41. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/docker_artifact.py +0 -0
  42. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/evidence_store.py +0 -0
  43. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/maintainability.py +0 -0
  44. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/models.py +0 -0
  45. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policies/__init__.py +0 -0
  46. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/profiles/__init__.py +0 -0
  47. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/profiles/minimal.json +0 -0
  48. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/query.py +0 -0
  49. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_bundle.py +0 -0
  50. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_candidate.py +0 -0
  51. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_certification.py +0 -0
  52. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_publication.py +0 -0
  53. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_qualification.py +0 -0
  54. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/repository_qualification.py +0 -0
  55. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime_qualification.py +0 -0
  56. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/__init__.py +0 -0
  57. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/assessment-decision-evidence.json +0 -0
  58. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/assessment-evidence.json +0 -0
  59. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/capability-evidence.json +0 -0
  60. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/common.schema.json +0 -0
  61. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/differential-evidence.json +0 -0
  62. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/policy-evidence.json +0 -0
  63. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/schemas/repository-qualification-evidence.json +0 -0
  64. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/software_assurance.py +0 -0
  65. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trend.py +0 -0
  66. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trusted_delivery.py +0 -0
  67. {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
  68. {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
  69. {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
  70. {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
  71. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_baseline.py +0 -0
  72. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_build_reproducibility.py +0 -0
  73. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_capability_platform.py +0 -0
  74. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_complexity.py +0 -0
  75. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_docker_artifact.py +0 -0
  76. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_evidence_store.py +0 -0
  77. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_execution.py +0 -0
  78. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_query.py +0 -0
  79. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_candidate.py +0 -0
  80. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_certification.py +0 -0
  81. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime_qualification.py +0 -0
  82. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_software_assurance.py +0 -0
  83. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trend.py +0 -0
  84. {technical_debt_engine_runtime-0.2.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trusted_delivery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: technical-debt-engine-runtime
3
- Version: 0.2.0
3
+ Version: 1.0.0
4
4
  Summary: Technical Debt Engine runtime foundation API
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: radon==6.0.1
@@ -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==80.9.0"]
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.2.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"]
@@ -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.2.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 .dependency_health import CAPABILITY_ID as DEPENDENCY_CAPABILITY_ID, CAPABILITY_VERSION as DEPENDENCY_CAPABILITY_VERSION, discover as discover_dependencies
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 == DEPENDENCY_CAPABILITY_ID:
116
- result = discover_dependencies(context.repository_root)
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
- capability = {"capabilityId": DEPENDENCY_CAPABILITY_ID, "capabilityVersion": DEPENDENCY_CAPABILITY_VERSION,
119
- "status": "VALID", "adapterIds": [], "completeness": 1, "qualificationApplicable": True,
120
- "executionTiming": {"durationMs": duration}}
121
- return {"measurements": result["measurements"], "findings": result["findings"], "capabilityResults": [capability]}
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