technical-debt-engine-runtime 0.1.0__tar.gz → 1.0.0rc3__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.1.0 → technical_debt_engine_runtime-1.0.0rc3}/PKG-INFO +2 -1
  2. technical_debt_engine_runtime-1.0.0rc3/README.md +44 -0
  3. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/pyproject.toml +4 -3
  4. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_cli/main.py +141 -76
  5. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/__init__.py +2 -1
  6. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/analyzer_discovery.py +21 -0
  7. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/assessment.py +33 -0
  8. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/baseline.py +6 -3
  9. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/code_size.py +9 -11
  10. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/complexity.py +6 -11
  11. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/configuration.py +19 -2
  12. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/coverage.py +210 -0
  13. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/dependency_health.py +173 -0
  14. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/differential.py +50 -0
  15. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/evidence_store.py +9 -1
  16. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/execution.py +98 -24
  17. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/policies/generation-1.json +18 -0
  18. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/policy.py +106 -8
  19. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/profiles/__init__.py +1 -0
  20. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/profiles/minimal.json +10 -0
  21. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/profiles/standard.json +13 -0
  22. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/registries.py +110 -0
  23. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/repository_qualification.py +119 -0
  24. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/runtime.py +100 -25
  25. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/runtime_qualification.py +4 -2
  26. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/__init__.py +88 -0
  27. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/assessment-decision-evidence.json +8 -0
  28. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/assessment-evidence.json +8 -0
  29. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/capability-evidence.json +8 -0
  30. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/common.schema.json +7 -0
  31. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/differential-evidence.json +1 -0
  32. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/policy-evidence.json +8 -0
  33. technical_debt_engine_runtime-1.0.0rc3/src/tde_runtime/schemas/repository-qualification-evidence.json +8 -0
  34. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/trend.py +3 -1
  35. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/technical_debt_engine_runtime.egg-info/PKG-INFO +2 -1
  36. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/technical_debt_engine_runtime.egg-info/SOURCES.txt +22 -0
  37. technical_debt_engine_runtime-1.0.0rc3/src/technical_debt_engine_runtime.egg-info/requires.txt +1 -0
  38. technical_debt_engine_runtime-1.0.0rc3/tests/test_capability_platform.py +21 -0
  39. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_cli.py +40 -8
  40. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_code_size.py +35 -10
  41. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_complexity.py +3 -3
  42. technical_debt_engine_runtime-1.0.0rc3/tests/test_coverage.py +118 -0
  43. technical_debt_engine_runtime-1.0.0rc3/tests/test_dependency_health.py +138 -0
  44. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_policy.py +31 -0
  45. technical_debt_engine_runtime-1.0.0rc3/tests/test_public_artifacts.py +152 -0
  46. technical_debt_engine_runtime-1.0.0rc3/tests/test_public_policy_configuration.py +215 -0
  47. technical_debt_engine_runtime-1.0.0rc3/tests/test_release_bundle.py +40 -0
  48. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_release_publication.py +12 -7
  49. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_release_qualification.py +10 -1
  50. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_runtime.py +36 -8
  51. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_runtime_qualification.py +1 -1
  52. technical_debt_engine_runtime-0.1.0/README.md +0 -30
  53. technical_debt_engine_runtime-0.1.0/src/tde_runtime/dependency_health.py +0 -23
  54. technical_debt_engine_runtime-0.1.0/src/tde_runtime/policies/generation-1.json +0 -18
  55. technical_debt_engine_runtime-0.1.0/src/tde_runtime/registries.py +0 -21
  56. technical_debt_engine_runtime-0.1.0/tests/test_release_bundle.py +0 -25
  57. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/setup.cfg +0 -0
  58. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_cli/__init__.py +0 -0
  59. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/docker_artifact.py +0 -0
  60. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/maintainability.py +0 -0
  61. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/models.py +0 -0
  62. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/policies/__init__.py +0 -0
  63. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/query.py +0 -0
  64. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/release_bundle.py +0 -0
  65. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/release_candidate.py +0 -0
  66. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/release_certification.py +0 -0
  67. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/release_publication.py +0 -0
  68. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/release_qualification.py +0 -0
  69. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/software_assurance.py +0 -0
  70. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/tde_runtime/trusted_delivery.py +0 -0
  71. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/technical_debt_engine_runtime.egg-info/dependency_links.txt +0 -0
  72. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/technical_debt_engine_runtime.egg-info/entry_points.txt +0 -0
  73. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/src/technical_debt_engine_runtime.egg-info/top_level.txt +0 -0
  74. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_baseline.py +0 -0
  75. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_build_reproducibility.py +0 -0
  76. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_docker_artifact.py +0 -0
  77. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_evidence_store.py +0 -0
  78. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_execution.py +0 -0
  79. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_query.py +0 -0
  80. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_release_candidate.py +0 -0
  81. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_release_certification.py +0 -0
  82. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_software_assurance.py +0 -0
  83. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_trend.py +0 -0
  84. {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0rc3}/tests/test_trusted_delivery.py +0 -0
@@ -1,5 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: technical-debt-engine-runtime
3
- Version: 0.1.0
3
+ Version: 1.0.0rc3
4
4
  Summary: Technical Debt Engine runtime foundation API
5
5
  Requires-Python: >=3.11
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,12 +1,13 @@
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.1.0"
7
+ version = "1.0.0rc3"
8
8
  description = "Technical Debt Engine runtime foundation API"
9
9
  requires-python = ">=3.11"
10
+ dependencies = ["radon==6.0.1"]
10
11
 
11
12
  [project.scripts]
12
13
  tde = "tde_cli.main:console_main"
@@ -15,7 +16,7 @@ tde = "tde_cli.main:console_main"
15
16
  where = ["src"]
16
17
 
17
18
  [tool.setuptools.package-data]
18
- tde_runtime = ["policies/*.json"]
19
+ tde_runtime = ["policies/*.json", "profiles/*.json", "schemas/*.json"]
19
20
 
20
21
  [tool.unittest]
21
22
  start-directory = "tests"
@@ -10,6 +10,7 @@ import sys
10
10
  from typing import Any, Sequence, TextIO
11
11
 
12
12
  from tde_runtime import Runtime, RuntimeConfiguration
13
+ from tde_runtime.assessment import AssessmentError, AssessmentOrchestrator
13
14
  from tde_runtime.baseline import BaselineError, BaselineRepository, ComparisonEngine, ComparisonRepository
14
15
  from tde_runtime.policy import PolicyEngine, PolicyError
15
16
  from tde_runtime.trend import TrendEngine
@@ -20,18 +21,28 @@ from tde_runtime.trusted_delivery import TrustedDelivery
20
21
  from tde_runtime.release_qualification import ReleaseQualification
21
22
  from tde_runtime.release_certification import ReleaseCertification
22
23
  from tde_runtime.runtime import EVIDENCE_SCHEMA_VERSION, RUNTIME_VERSION
24
+ from tde_runtime.schemas import SchemaRegistry
25
+ from tde_runtime.repository_qualification import (QualificationRegistry,
26
+ RepositoryDefinitionRegistry,
27
+ RepositoryQualification,
28
+ RepositoryQualificationError)
29
+ from tde_runtime.differential import AssessmentBaselineRegistry, DifferentialEngine, DifferentialError
23
30
 
24
31
 
25
- CLI_VERSION = "0.1.0"
32
+ CLI_VERSION = "1.0.0rc3"
26
33
  GENERATION = "1"
27
34
 
28
35
 
29
36
  class ExitCode:
30
37
  SUCCESS = 0
31
38
  WARNING = 1
32
- FAILED = 2
33
- BLOCKED = 3
39
+ FAILED_CLOSED = 2
40
+ EXECUTION_ERROR = 3
34
41
  NOT_SUPPORTED = 4
42
+ ANALYZER_NOT_FOUND = 5
43
+ # Compatibility aliases for existing non-runtime commands.
44
+ FAILED = FAILED_CLOSED
45
+ BLOCKED = EXECUTION_ERROR
35
46
 
36
47
 
37
48
  def _policy_exit_code(decision: str) -> int:
@@ -46,8 +57,10 @@ COMMANDS: dict[str, dict[str, str]] = {
46
57
  "validate": {"purpose": "Validate runtime configuration and context."},
47
58
  "inspect": {"purpose": "Inspect a target and planned Code Size execution."},
48
59
  "assess": {"purpose": "Assess a target through selected capabilities."},
60
+ "schema": {"purpose": "List the public, versioned assessment schemas."},
49
61
  "baseline": {"purpose": "Create an immutable baseline from persisted canonical evidence."},
50
62
  "compare": {"purpose": "Persist and qualify a canonical comparison against a baseline."},
63
+ "diff": {"purpose": "Compare a current assessment with an immutable baseline."},
51
64
  "trend": {"purpose": "Aggregate canonical evidence history into trends."},
52
65
  "query": {"purpose": "Query canonical engineering evidence."},
53
66
  "store": {"purpose": "Persist canonical Runtime evidence."},
@@ -70,11 +83,14 @@ def _global_options(parser: argparse.ArgumentParser) -> None:
70
83
  parser.add_argument("--output", help="Output destination (reserved; console is used now).")
71
84
  parser.add_argument("--format", choices=("human", "json", "markdown"), default="human", help="Output format.")
72
85
  parser.add_argument("--log-level", choices=("ERROR", "WARNING", "INFO", "DEBUG", "TRACE"), help="Logging level.")
73
- parser.add_argument("--policy", help="Repository or workspace policy directory.")
86
+ parser.add_argument("--policy", metavar="FILE", help="Declarative policy configuration file.")
74
87
  parser.add_argument("--policy-override", action="append", default=[], metavar="RULE=JSON", help="Override a policy rule with a JSON object.")
88
+ parser.add_argument("--profile", help="Declarative assessment profile for tde assess.")
75
89
  parser.add_argument("--baseline-location", help="Directory used for immutable baselines.")
76
90
  parser.add_argument("--history-depth", type=int, help="Maximum baseline history depth for trends.")
77
91
  parser.add_argument("--store-location", help="Directory used for canonical evidence storage.")
92
+ parser.add_argument("--repository-definition", metavar="FILE", help="Declarative repository definition for tde qualify.")
93
+ parser.add_argument("--qualification-location", help="Directory used for immutable repository qualification records.")
78
94
 
79
95
 
80
96
  def build_parser() -> argparse.ArgumentParser:
@@ -88,7 +104,7 @@ def build_parser() -> argparse.ArgumentParser:
88
104
  command.add_argument("--capability", action="append", default=[], help="Enable a registered capability.")
89
105
  if identifier == "baseline":
90
106
  command.add_argument("--name", help="Immutable baseline name.")
91
- if identifier == "compare":
107
+ if identifier in {"compare", "assess", "diff"}:
92
108
  command.add_argument("--baseline", help="Baseline name or JSON path.")
93
109
  if identifier == "query":
94
110
  command.add_argument("--resource", default="repositories", help="Evidence resource.")
@@ -139,6 +155,11 @@ def _store_location(arguments: argparse.Namespace, target: str) -> Path:
139
155
  return location if location.is_absolute() else Path(target) / location
140
156
 
141
157
 
158
+ def _qualification_location(arguments: argparse.Namespace, target: str) -> Path:
159
+ location = Path(arguments.qualification_location or ".tde/qualifications")
160
+ return location if location.is_absolute() else Path(target) / location
161
+
162
+
142
163
  def _runtime_result(command: str, target: str, configuration: RuntimeConfiguration,
143
164
  store: EvidenceStore | None = None) -> tuple[int, dict[str, Any]]:
144
165
  result = Runtime().execute(target, configuration)
@@ -146,14 +167,23 @@ def _runtime_result(command: str, target: str, configuration: RuntimeConfigurati
146
167
  "execution": result.report["executionSummary"], "environment": result.report["environment"], "qualification": result.report["qualification"],
147
168
  "runtimeQualification": result.evidence["runtimeQualification"], "validation": result.validation,
148
169
  "evidence": result.evidence, "evidenceId": result.evidence["integrity"]["contentDigest"]}
170
+ capability_statuses = {item.get("status") for item in result.evidence.get("capabilityResults", [])}
171
+ if "NOT_SUPPORTED" in capability_statuses:
172
+ return ExitCode.NOT_SUPPORTED, payload
173
+ if "ANALYZER_NOT_FOUND" in capability_statuses:
174
+ return ExitCode.ANALYZER_NOT_FOUND, payload
175
+ if "FAILED_CLOSED" in capability_statuses:
176
+ return ExitCode.FAILED_CLOSED, payload
149
177
  if store is not None:
150
178
  payload["evidenceStore"] = store.persist(result.evidence)
151
- decision = result.evidence["policyEvidence"]["decision"]
152
- if decision == "BLOCKED":
153
- return ExitCode.BLOCKED, payload
154
- if command in {"assess", "run"} and result.evidence["runtimeQualification"]["level"] != "QUALIFIED":
155
- return ExitCode.BLOCKED, payload
156
- return (_policy_exit_code(decision) if command in {"assess", "run"} else ExitCode.SUCCESS), payload
179
+ if command in {"assess", "run"}:
180
+ # Assessment exit codes describe the public execution contract. Policy
181
+ # outcomes are preserved in canonical evidence for consumers to apply;
182
+ # a policy threshold must not masquerade as an analyzer failure.
183
+ if result.evidence["runtimeQualification"]["level"] != "QUALIFIED":
184
+ return ExitCode.EXECUTION_ERROR, payload
185
+ return _policy_exit_code(result.evidence["assessmentDecision"]["decision"]), payload
186
+ return ExitCode.SUCCESS, payload
157
187
 
158
188
 
159
189
  def _render_capability_report(evidence: dict[str, Any], capability: str, output_format: str, stream: TextIO) -> None:
@@ -176,6 +206,9 @@ def main(argv: Sequence[str] | None = None, stdout: TextIO | None = None) -> int
176
206
  stream = stdout or sys.stdout
177
207
  parser = build_parser()
178
208
  arguments = parser.parse_args(argv)
209
+ if arguments.command == "schema":
210
+ _render({"command": "schema", "schemas": list(SchemaRegistry.catalogue())}, arguments.format, stream)
211
+ return ExitCode.SUCCESS
179
212
  prepared = _prepare_command(arguments, parser, stream)
180
213
  if isinstance(prepared, int): return prepared
181
214
  configuration = prepared
@@ -195,72 +228,53 @@ def main(argv: Sequence[str] | None = None, stdout: TextIO | None = None) -> int
195
228
 
196
229
 
197
230
  def _prepare_command(arguments: argparse.Namespace, parser: argparse.ArgumentParser, stream: TextIO) -> RuntimeConfiguration | int:
198
- if arguments.version:
199
- _render({"cliVersion": CLI_VERSION, "runtimeVersion": RUNTIME_VERSION,
200
- "schemaVersion": EVIDENCE_SCHEMA_VERSION, "generation": GENERATION}, arguments.format, stream)
201
- return ExitCode.SUCCESS
202
- if not arguments.command:
203
- parser.print_help(stream)
204
- return ExitCode.SUCCESS
205
- if arguments.command == "help":
206
- parser.print_help(stream)
231
+ if arguments.version or not arguments.command or arguments.command == "help":
232
+ if arguments.version:
233
+ _render({"cliVersion": CLI_VERSION, "runtimeVersion": RUNTIME_VERSION, "schemaVersion": EVIDENCE_SCHEMA_VERSION, "generation": GENERATION}, arguments.format, stream)
234
+ else:
235
+ parser.print_help(stream)
207
236
  return ExitCode.SUCCESS
208
237
  _configure_logging(arguments)
209
238
  try:
210
- configuration = _load_configuration(arguments.config, arguments.target)
211
- except ValueError as error:
212
- _render({"status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
239
+ configuration = _apply_options(_load_configuration(arguments.config, arguments.target), arguments)
240
+ return _apply_command_configuration(configuration, arguments)
241
+ except (ValueError, AssessmentError) as error:
242
+ _render({"command": arguments.command, "status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
213
243
  return ExitCode.BLOCKED
244
+
245
+
246
+ def _apply_options(configuration: RuntimeConfiguration, arguments: argparse.Namespace) -> RuntimeConfiguration:
214
247
  if arguments.policy or arguments.policy_override:
215
- values = configuration.as_dict()
216
- policy = dict(values["executionOptions"].get("policy", {}))
217
- if arguments.policy:
218
- policy["repository"] = arguments.policy
248
+ values = configuration.as_dict(); policy = dict(values["executionOptions"].get("policy", {})); policy.update({"file": arguments.policy} if arguments.policy else {})
219
249
  overrides = dict(policy.get("overrides", {}))
220
- try:
221
- for item in arguments.policy_override:
222
- rule, raw_value = item.split("=", 1)
223
- overrides[rule] = json.loads(raw_value)
224
- if not isinstance(overrides[rule], dict):
225
- raise ValueError("override must be a JSON object")
226
- except (ValueError, json.JSONDecodeError) as error:
227
- _render({"status": "BLOCKED", "reason": f"invalid policy override: {error}"}, arguments.format, stream)
228
- return ExitCode.BLOCKED
229
- policy["overrides"] = overrides
230
- values["policy"] = policy
231
- values["executionOptions"].pop("policy", None)
232
- configuration = RuntimeConfiguration.load(values)
250
+ for item in arguments.policy_override:
251
+ rule, raw_value = item.split("=", 1); overrides[rule] = json.loads(raw_value)
252
+ if not isinstance(overrides[rule], dict): raise ValueError("invalid policy override: override must be a JSON object")
253
+ policy["overrides"] = overrides; values["policy"] = policy; values["executionOptions"].pop("policy", None); configuration = RuntimeConfiguration.load(values)
233
254
  if arguments.baseline_location:
234
- values = configuration.as_dict()
235
- values["baseline"] = {"location": arguments.baseline_location}
236
- values["executionOptions"].pop("baseline", None)
237
- configuration = RuntimeConfiguration.load(values)
255
+ values = configuration.as_dict(); values["baseline"] = {"location": arguments.baseline_location}; values["executionOptions"].pop("baseline", None); configuration = RuntimeConfiguration.load(values)
238
256
  if arguments.history_depth is not None:
239
- if arguments.history_depth < 0:
240
- _render({"status": "BLOCKED", "reason": "history depth must not be negative"}, arguments.format, stream)
241
- return ExitCode.BLOCKED
242
- values = configuration.as_dict()
243
- values["trend"] = {"historyDepth": arguments.history_depth}
244
- values["executionOptions"].pop("trend", None)
245
- configuration = RuntimeConfiguration.load(values)
246
- if arguments.command in {"assess", "baseline", "compare", "run", "store", "inspect", "validate", "qualify", "report"} and arguments.capability:
247
- supported = {"code-size": "code_size", "complexity": "complexity"}
248
- if len(arguments.capability) != 1 or arguments.capability[0] not in supported:
249
- _render({"command": arguments.command, "status": "NOT_SUPPORTED", "reason": "This vertical slice supports code-size and complexity."}, arguments.format, stream)
250
- return ExitCode.NOT_SUPPORTED
251
- configuration = configuration.with_capability(supported[arguments.capability[0]])
257
+ if arguments.history_depth < 0: raise ValueError("history depth must not be negative")
258
+ values = configuration.as_dict(); values["trend"] = {"historyDepth": arguments.history_depth}; values["executionOptions"].pop("trend", None); configuration = RuntimeConfiguration.load(values)
259
+ return configuration
260
+
261
+
262
+ def _apply_command_configuration(configuration: RuntimeConfiguration, arguments: argparse.Namespace) -> RuntimeConfiguration:
263
+ if arguments.command in {"assess", "baseline", "compare", "run", "store", "inspect", "validate", "qualify", "report"}:
264
+ for capability in arguments.capability: configuration = configuration.with_capability(capability.replace("-", "_"))
265
+ if arguments.command == "assess":
266
+ return AssessmentOrchestrator().configure(configuration, profile=arguments.profile, capabilities=tuple(item.replace("-", "_") for item in arguments.capability))
252
267
  if arguments.command == "release-qualify":
253
- supported = {"code-size": "code_size", "complexity": "complexity"}
254
- if not arguments.release_capability or any(item not in supported for item in arguments.release_capability):
255
- _render({"command": "release-qualify", "status": "BLOCKED",
256
- "reason": "release-qualify requires one or more supported --release-capability values"}, arguments.format, stream)
257
- return ExitCode.BLOCKED
258
- for capability in sorted(set(arguments.release_capability)):
259
- configuration = configuration.with_capability(supported[capability])
268
+ supported = {"code-size": "code_size", "complexity": "complexity", "coverage": "coverage",
269
+ "dependency-health": "dependency_health"}
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")
271
+ for capability in sorted(set(arguments.release_capability)): configuration = configuration.with_capability(supported[capability])
260
272
  return configuration
261
273
 
262
274
 
263
275
  def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
276
+ if arguments.command in {"diff"} or (arguments.command == "assess" and arguments.baseline):
277
+ return _differential_command(arguments, configuration, stream)
264
278
  if arguments.command in {"baseline", "compare"}:
265
279
  return _baseline_command(arguments, configuration, stream)
266
280
  if arguments.command == "trend":
@@ -268,14 +282,7 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
268
282
  if arguments.command == "query":
269
283
  return _query_command(arguments, configuration, stream)
270
284
  if arguments.command == "qualify":
271
- try:
272
- result=Runtime().execute(arguments.target,configuration)
273
- capability=arguments.capability[0].replace("-","_") if arguments.capability else None
274
- qualification=RuntimeQualificationEngine().qualify(result.evidence,capability)
275
- _render({"command":"qualify","runtimeQualification":qualification},arguments.format,stream)
276
- return ExitCode.SUCCESS if qualification["level"]!="BLOCKED" else ExitCode.BLOCKED
277
- except ValueError as error:
278
- _render({"command":"qualify","status":"BLOCKED","reason":str(error)},arguments.format,stream); return ExitCode.BLOCKED
285
+ return _qualification_command(arguments, configuration, stream)
279
286
  if arguments.command == "report":
280
287
  return _report_command(arguments, stream)
281
288
  if arguments.command == "assure":
@@ -300,13 +307,13 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
300
307
  return ExitCode.SUCCESS if evidence["decision"] == "RELEASE_CERTIFIED" else ExitCode.BLOCKED
301
308
  if arguments.command in {"validate", "inspect", "assess", "run"}:
302
309
  if arguments.command in {"assess", "run"}:
303
- if not arguments.capability:
304
- _render({"command": "assess", "status": "NOT_IMPLEMENTED", "reason": "Only code-size and complexity are delivered."}, arguments.format, stream)
310
+ if arguments.command == "run" and not arguments.capability:
311
+ _render({"command": "assess", "status": "NOT_SUPPORTED", "reason": "assess requires an explicit capability."}, arguments.format, stream)
305
312
  return ExitCode.NOT_SUPPORTED
306
313
  try:
307
314
  store = EvidenceStore(_store_location(arguments, arguments.target)) if arguments.command in {"assess", "run"} else None
308
315
  code, payload = _runtime_result(arguments.command, arguments.target, configuration, store)
309
- except (ValueError, OSError, json.JSONDecodeError) as error:
316
+ except (PolicyError, ValueError, OSError, json.JSONDecodeError) as error:
310
317
  _render({"status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
311
318
  return ExitCode.BLOCKED
312
319
  _render(payload, arguments.format, stream)
@@ -316,6 +323,61 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
316
323
  return ExitCode.NOT_SUPPORTED
317
324
 
318
325
 
326
+ def _differential_command(arguments, configuration, stream):
327
+ location = configuration.execution_options.get("baseline", {}).get("location", ".tde/baselines")
328
+ registry = AssessmentBaselineRegistry(Path(arguments.target) / location if not Path(location).is_absolute() else location)
329
+ store = EvidenceStore(_store_location(arguments, arguments.target))
330
+ try:
331
+ if not arguments.baseline:
332
+ raise DifferentialError("diff requires --baseline")
333
+ current = Runtime().execute(arguments.target, configuration)
334
+ current_record = store.persist(current.evidence)
335
+ baseline = registry.load(arguments.baseline)
336
+ previous = store.retrieve(baseline["assessmentEvidenceId"].removeprefix("sha256:"))["evidence"]
337
+ differential = DifferentialEngine().compare(current.evidence, baseline, previous)
338
+ _render({"command": "diff", "baseline": baseline, "assessmentEvidenceStore": current_record,
339
+ "assessmentEvidence": current.evidence, "differentialEvidence": differential}, arguments.format, stream)
340
+ return _policy_exit_code(current.evidence["assessmentDecision"]["decision"])
341
+ except (DifferentialError, ValueError, OSError, json.JSONDecodeError) as error:
342
+ _render({"command": "diff", "status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
343
+ return ExitCode.BLOCKED
344
+
345
+
346
+ def _qualification_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
347
+ """Run a profile-driven assessment and register its separate qualification result."""
348
+ try:
349
+ definition = RepositoryDefinitionRegistry().resolve(arguments.target, arguments.repository_definition)
350
+ profile = arguments.profile or definition["defaultAssessmentProfile"]
351
+ configuration = AssessmentOrchestrator().configure(
352
+ configuration, profile=profile,
353
+ capabilities=tuple(item.replace("-", "_") for item in arguments.capability),
354
+ )
355
+ target = str(definition["repositoryRoot"])
356
+ result = Runtime().execute(target, configuration)
357
+ qualification = RepositoryQualification.create(definition, result.evidence)
358
+ assessment_record = EvidenceStore(_store_location(arguments, target)).persist(result.evidence)
359
+ registry_record = QualificationRegistry(_qualification_location(arguments, target)).persist(qualification)
360
+ payload = {"command": "qualify", "repositoryQualification": qualification,
361
+ "qualificationRegistry": registry_record, "assessmentEvidence": result.evidence,
362
+ "assessmentEvidenceStore": assessment_record}
363
+ statuses = {item.get("status") for item in result.evidence.get("capabilityResults", [])}
364
+ if "NOT_SUPPORTED" in statuses:
365
+ code = ExitCode.NOT_SUPPORTED
366
+ elif "ANALYZER_NOT_FOUND" in statuses:
367
+ code = ExitCode.ANALYZER_NOT_FOUND
368
+ elif "FAILED_CLOSED" in statuses:
369
+ code = ExitCode.FAILED_CLOSED
370
+ elif qualification["qualificationStatus"] == "BLOCKED":
371
+ code = ExitCode.EXECUTION_ERROR
372
+ else:
373
+ code = _policy_exit_code(qualification["assessmentDecision"])
374
+ _render(payload, arguments.format, stream)
375
+ return code
376
+ except (RepositoryQualificationError, PolicyError, ValueError, OSError, json.JSONDecodeError) as error:
377
+ _render({"command": "qualify", "status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
378
+ return ExitCode.BLOCKED
379
+
380
+
319
381
  def _baseline_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
320
382
  location = configuration.execution_options.get("baseline", {}).get("location", ".tde/baselines")
321
383
  repository = BaselineRepository(Path(arguments.target) / location if not Path(location).is_absolute() else location)
@@ -332,7 +394,7 @@ def _baseline_command(arguments: argparse.Namespace, configuration: RuntimeConfi
332
394
 
333
395
 
334
396
  def _create_baseline(arguments, repository, evidence, persisted, stream):
335
- baseline = repository.create(evidence, arguments.name)
397
+ baseline = AssessmentBaselineRegistry(repository.location).create(evidence, arguments.name)
336
398
  _render({"command": "baseline", "status": "VALID", "evidenceStore": persisted, "baseline": baseline}, arguments.format, stream)
337
399
  return ExitCode.SUCCESS
338
400
 
@@ -340,6 +402,9 @@ def _create_baseline(arguments, repository, evidence, persisted, stream):
340
402
  def _compare_baseline(arguments, repository, current, evidence, persisted, stream):
341
403
  if not arguments.baseline: raise BaselineError("compare requires --baseline")
342
404
  baseline = repository.load(arguments.baseline)
405
+ if "evidence" not in baseline:
406
+ baseline = {**baseline, "evidence": EvidenceStore(_store_location(arguments, arguments.target)).retrieve(
407
+ baseline["assessmentEvidenceId"].removeprefix("sha256:"))["evidence"]}
343
408
  comparison = ComparisonEngine().compare(evidence, baseline)
344
409
  policy = PolicyEngine().load(current.context.configuration, current.context.repository_root, current.context.runtime_version, current.context.schema_version)
345
410
  normalized = {"measurements": evidence["measurements"], "findings": evidence["findings"], "capabilityResults": evidence["capabilityResults"], "comparison": comparison}
@@ -2,5 +2,6 @@
2
2
 
3
3
  from .configuration import RuntimeConfiguration
4
4
  from .runtime import Runtime
5
+ from .assessment import AssessmentOrchestrator
5
6
 
6
- __all__ = ["Runtime", "RuntimeConfiguration"]
7
+ __all__ = ["AssessmentOrchestrator", "Runtime", "RuntimeConfiguration"]
@@ -0,0 +1,21 @@
1
+ """Generic executable discovery for Runtime analyzer adapters."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from typing import Any
8
+
9
+
10
+ def discover(executable_name: str, minimum_version: tuple[int, int], timeout: int) -> dict[str, Any]:
11
+ executable = shutil.which(executable_name)
12
+ if not executable:
13
+ return {"status": "ANALYZER_NOT_FOUND", "limitation": {"id": f"analyzer.{executable_name}.unavailable", "description": f"{executable_name} is not on PATH.", "cause": "analyzer unavailable"}}
14
+ try:
15
+ version = subprocess.run([executable, "--version"], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip()
16
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as error:
17
+ return {"status": "FAILED_CLOSED", "limitation": {"id": f"analyzer.{executable_name}.discovery_failed", "description": str(error), "cause": "analyzer discovery failed"}}
18
+ match = re.search(r"(\d+)\.(\d+)", version)
19
+ if not match or tuple(map(int, match.groups())) < minimum_version:
20
+ return {"status": "ANALYZER_NOT_FOUND", "limitation": {"id": f"analyzer.{executable_name}.unsupported_version", "description": f"{executable_name} {minimum_version[0]}.{minimum_version[1]}+ is required; found {version or 'unknown'}.", "cause": "unsupported analyzer version"}}
21
+ return {"status": "VALID", "executable": executable, "version": version}
@@ -0,0 +1,33 @@
1
+ """Profile-driven assessment orchestration without analyzer knowledge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .configuration import RuntimeConfiguration
6
+ from .registries import AssessmentProfileRegistry
7
+
8
+
9
+ class AssessmentError(ValueError):
10
+ """Raised when an assessment cannot be planned from registered profiles."""
11
+
12
+
13
+ class AssessmentOrchestrator:
14
+ """Selects the assessment capability set before the Runtime executes it."""
15
+
16
+ def __init__(self, profiles: AssessmentProfileRegistry | None = None) -> None:
17
+ self._profiles = profiles or AssessmentProfileRegistry()
18
+
19
+ def configure(self, configuration: RuntimeConfiguration, *, profile: str | None = None,
20
+ capabilities: tuple[str, ...] = ()) -> RuntimeConfiguration:
21
+ if capabilities:
22
+ return configuration.with_assessment_profile("explicit", capabilities)
23
+ try:
24
+ selected = self._profiles.resolve(profile)
25
+ except ValueError as error:
26
+ raise AssessmentError(str(error)) from error
27
+ if selected is None:
28
+ label = profile if profile is not None else "default"
29
+ raise AssessmentError(f"assessment profile is not registered: {label}")
30
+ capabilities = tuple(item["identifier"] for item in selected["capabilities"])
31
+ return configuration.with_assessment_profile(str(selected["identifier"]), capabilities,
32
+ identity=dict(selected["identity"]),
33
+ policy_file=str(selected["policyFile"]))
@@ -47,9 +47,12 @@ class BaselineRepository:
47
47
  value = json.loads(path.read_text(encoding="utf-8"))
48
48
  except (OSError, json.JSONDecodeError) as error:
49
49
  raise BaselineError(f"cannot load baseline {reference}: {error}") from error
50
- if not isinstance(value, dict) or "evidence" not in value:
51
- raise BaselineError("baseline does not contain canonical evidence")
52
- self._validate_evidence(value["evidence"])
50
+ if not isinstance(value, dict):
51
+ raise BaselineError("baseline is malformed")
52
+ if "evidence" in value:
53
+ self._validate_evidence(value["evidence"])
54
+ elif not {"assessmentEvidenceId", "repositoryId", "baselineId"} <= set(value):
55
+ raise BaselineError("baseline does not reference canonical evidence")
53
56
  return value
54
57
 
55
58
  @staticmethod
@@ -1,16 +1,18 @@
1
1
  """Code Size capability adapter backed by an explicitly installed cloc executable."""
2
2
  from __future__ import annotations
3
- import json, re, shutil, subprocess
3
+ import json, subprocess
4
4
  from collections import defaultdict
5
5
  from hashlib import sha256
6
6
  from pathlib import Path
7
7
  from typing import Any
8
+ from .analyzer_discovery import discover
8
9
 
9
10
  CAPABILITY_ID = "code_size"
10
11
  CAPABILITY_VERSION = "0.1.0"
11
12
  ADAPTER_ID = "code_size.cloc"
12
13
  ADAPTER_VERSION = "0.1.0"
13
14
  MINIMUM_ANALYZER_VERSION = (2, 10)
15
+ EXCLUDED_DIRECTORIES = (".git", ".tde", "__pycache__", ".venv", "venv", "build", "dist", "bin", "obj", ".build", ".swiftpm", ".pio", ".release", ".public-release")
14
16
 
15
17
  def classify(path: str) -> str:
16
18
  value = path.replace("\\", "/").lower()
@@ -22,19 +24,15 @@ def classify(path: str) -> str:
22
24
  return "SOURCE"
23
25
 
24
26
  def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
25
- executable = shutil.which("cloc")
26
- if not executable:
27
- return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.unavailable","description":"cloc is not on PATH; install cloc 2.10+.","cause":"analyzer unavailable"}]}
27
+ discovery = discover("cloc", MINIMUM_ANALYZER_VERSION, timeout)
28
+ if discovery["status"] != "VALID":
29
+ return {"status": discovery["status"], "limitations": [discovery["limitation"]]}
28
30
  try:
29
- version = subprocess.run([executable, "--version"], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip()
30
- match = re.search(r"(\d+)\.(\d+)", version)
31
- if not match or tuple(map(int, match.groups())) < MINIMUM_ANALYZER_VERSION:
32
- return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.unsupported_version","description":f"cloc {MINIMUM_ANALYZER_VERSION[0]}.{MINIMUM_ANALYZER_VERSION[1]}+ is required; found {version or 'unknown'}.","cause":"unsupported analyzer version"}]}
33
- result = subprocess.run([executable, "--json", "--by-file", "--quiet", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
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)
34
32
  raw = result.stdout
35
33
  data = json.loads(raw)
36
34
  except (subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
37
- return {"status":"BLOCKED", "limitations":[{"id":"analyzer.cloc.failed","description":str(error),"cause":"analyzer execution failed"}]}
35
+ return {"status":"FAILED_CLOSED", "limitations":[{"id":"analyzer.cloc.failed","description":str(error),"cause":"analyzer execution failed"}]}
38
36
  files, languages = [], defaultdict(lambda: {"files":0,"code":0,"comment":0,"blank":0})
39
37
  totals = defaultdict(int)
40
38
  entries = ((name, item) for name, item in data.items() if name not in {"header", "SUM"})
@@ -46,4 +44,4 @@ def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
46
44
  for key in ("code","comment","blank"): totals[key] += item[key]
47
45
  totals["files"] += 1; totals[category.lower()] += item["code"]
48
46
  source = totals["source"]; ratio = totals["test"] / source if source else 0
49
- return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"cloc","version":version},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"files":files,"languages":dict(sorted(languages.items())),"totals":dict(totals),"testToSourceRatio":ratio,"limitations":[{"id":"code_size.logical_lines.unavailable","description":"cloc does not provide logical line counts.","cause":"analyzer limitation"}]}
47
+ return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"cloc","version":discovery["version"]},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"files":files,"languages":dict(sorted(languages.items())),"totals":dict(totals),"testToSourceRatio":ratio,"limitations":[{"id":"code_size.logical_lines.unavailable","description":"cloc does not provide logical line counts.","cause":"analyzer limitation"}]}
@@ -4,12 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import fnmatch
6
6
  import json
7
- import re
8
- import shutil
9
7
  import subprocess
10
8
  from hashlib import sha256
11
9
  from pathlib import Path
12
10
  from typing import Any, Mapping
11
+ from .analyzer_discovery import discover
13
12
 
14
13
  CAPABILITY_ID = "complexity"
15
14
  CAPABILITY_VERSION = "0.1.0"
@@ -59,15 +58,11 @@ def _thresholds(configuration: Mapping[str, Any]) -> dict[str, int]:
59
58
  def analyze(root: Path, timeout: int = 60, configuration: Mapping[str, Any] | None = None) -> dict[str, Any]:
60
59
  """Execute Radon deterministically and retain native output for evidence."""
61
60
  configuration = configuration or {}
62
- executable = shutil.which("radon")
63
- if not executable:
64
- return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.unavailable","description":"radon is not on PATH; install Radon 6.0+.","cause":"analyzer unavailable"}]}
61
+ discovery = discover("radon", MINIMUM_ANALYZER_VERSION, timeout)
62
+ if discovery["status"] != "VALID":
63
+ return {"status": discovery["status"], "limitations": [discovery["limitation"]]}
65
64
  try:
66
- version = subprocess.run([executable, "--version"], capture_output=True, text=True, timeout=timeout, check=True).stdout.strip()
67
- match = re.search(r"(\d+)\.(\d+)", version)
68
- if not match or tuple(map(int, match.groups())) < MINIMUM_ANALYZER_VERSION:
69
- return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.unsupported_version","description":f"Radon 6.0+ is required; found {version or 'unknown'}.","cause":"unsupported analyzer version"}]}
70
- completed = subprocess.run([executable, "cc", "--json", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
65
+ completed = subprocess.run([discovery["executable"], "cc", "--json", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
71
66
  raw, data = _portable_native_output(root, json.loads(completed.stdout))
72
67
  except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
73
68
  return {"status":"BLOCKED", "limitations":[{"id":"analyzer.radon.failed","description":str(error),"cause":"analyzer execution failed"}]}
@@ -89,4 +84,4 @@ def analyze(root: Path, timeout: int = 60, configuration: Mapping[str, Any] | No
89
84
  limitations=[]
90
85
  if skipped: limitations.append({"id":"complexity.configuration.ignored","description":f"{skipped} symbol(s) were excluded by Complexity configuration.","cause":"configured exclusion"})
91
86
  if not symbols: limitations.append({"id":"complexity.python.no_symbols","description":"Radon found no supported Python symbols after exclusions.","cause":"analyzer capability limitation"})
92
- return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"radon","version":version},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"symbols":symbols,"thresholds":thresholds,"limitations":limitations}
87
+ return {"status":"VALID","adapter":{"id":ADAPTER_ID,"version":ADAPTER_VERSION},"analyzer":{"id":"radon","version":discovery["version"]},"rawOutput":raw,"rawOutputHash":"sha256:"+sha256(raw.encode()).hexdigest(),"symbols":symbols,"thresholds":thresholds,"limitations":limitations}