technical-debt-engine-runtime 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/PKG-INFO +2 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/README.md +8 -2
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/pyproject.toml +3 -2
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_cli/main.py +139 -75
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/__init__.py +2 -1
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/analyzer_discovery.py +21 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/assessment.py +33 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/baseline.py +6 -3
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/code_size.py +8 -11
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/complexity.py +6 -11
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/configuration.py +19 -2
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/differential.py +50 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/evidence_store.py +9 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/execution.py +37 -17
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/policies/generation-1.json +16 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/policy.py +104 -8
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/profiles/__init__.py +1 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/profiles/minimal.json +10 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/profiles/standard.json +11 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/registries.py +106 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/repository_qualification.py +119 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/runtime.py +91 -23
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/runtime_qualification.py +4 -2
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/__init__.py +88 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/assessment-decision-evidence.json +8 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/assessment-evidence.json +8 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/capability-evidence.json +8 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/common.schema.json +7 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/differential-evidence.json +1 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/policy-evidence.json +8 -0
- technical_debt_engine_runtime-0.2.0/src/tde_runtime/schemas/repository-qualification-evidence.json +8 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/trend.py +3 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/technical_debt_engine_runtime.egg-info/PKG-INFO +2 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/technical_debt_engine_runtime.egg-info/SOURCES.txt +19 -0
- technical_debt_engine_runtime-0.2.0/src/technical_debt_engine_runtime.egg-info/requires.txt +1 -0
- technical_debt_engine_runtime-0.2.0/tests/test_capability_platform.py +21 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_cli.py +40 -8
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_code_size.py +28 -10
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_complexity.py +3 -3
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_policy.py +21 -0
- technical_debt_engine_runtime-0.2.0/tests/test_public_artifacts.py +86 -0
- technical_debt_engine_runtime-0.2.0/tests/test_public_policy_configuration.py +214 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_release_bundle.py +16 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_release_publication.py +12 -7
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_runtime.py +20 -6
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_runtime_qualification.py +1 -1
- technical_debt_engine_runtime-0.1.0/src/tde_runtime/policies/generation-1.json +0 -18
- technical_debt_engine_runtime-0.1.0/src/tde_runtime/registries.py +0 -21
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/setup.cfg +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_cli/__init__.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/dependency_health.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/maintainability.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/models.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/policies/__init__.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/query.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/release_bundle.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/release_certification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/release_publication.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/release_qualification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_runtime/trusted_delivery.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/technical_debt_engine_runtime.egg-info/dependency_links.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/technical_debt_engine_runtime.egg-info/entry_points.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/technical_debt_engine_runtime.egg-info/top_level.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_baseline.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_build_reproducibility.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_evidence_store.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_execution.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_query.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_release_certification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_release_qualification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_trend.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/tests/test_trusted_delivery.py +0 -0
|
@@ -4,8 +4,13 @@ Technical Debt Engine (TDE) is a standalone, capability-based engineering platfo
|
|
|
4
4
|
|
|
5
5
|
TDE is project-independent, platform-independent, CI-independent, language-independent, and vendor-neutral. DJConnect is the first reference consumer; it does not own TDE's architecture, roadmap, or release lifecycle.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Release `0.1.0` is available as the PyPI distribution
|
|
8
|
+
[`technical-debt-engine-runtime`](https://pypi.org/project/technical-debt-engine-runtime/0.1.0/),
|
|
9
|
+
a [GitHub Release](https://github.com/pcvantol/technical-debt-engine/releases/tag/0.1.0),
|
|
10
|
+
and Docker image `docker.io/pcvantol/technical-debt-engine:0.1.0`. The Docker
|
|
11
|
+
OCI index is `sha256:aa648019045a442a0dbce029ee11ecb15c7755d845205fa8f07467e0faf18679`;
|
|
12
|
+
no `latest` tag exists. See [Internal Release Report](INTERNAL_RELEASE_REPORT.md)
|
|
13
|
+
for the immutable publication evidence and release boundary.
|
|
9
14
|
|
|
10
15
|
## Product contracts
|
|
11
16
|
|
|
@@ -20,6 +25,7 @@ Operational repository assurance is available through `tde assure`; see [SOFTWAR
|
|
|
20
25
|
- [Engineering method](ENGINEERING_METHOD.md) and [session bootstrap](BOOTSTRAP.md)
|
|
21
26
|
- [Capability model](CAPABILITY_MODEL.md)
|
|
22
27
|
- [CLI specification](CLI_SPECIFICATION.md)
|
|
28
|
+
- [Code Size Runtime contract](CODE_SIZE_RUNTIME.md)
|
|
23
29
|
- [Evidence schema](EVIDENCE_SCHEMA.md)
|
|
24
30
|
- [Qualification model](QUALIFICATION_MODEL.md)
|
|
25
31
|
- [Roadmap](PRODUCT_ROADMAP.md) and [backlog](PRODUCT_BACKLOG.md)
|
|
@@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "technical-debt-engine-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
{technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-0.2.0}/src/tde_cli/main.py
RENAMED
|
@@ -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.
|
|
32
|
+
CLI_VERSION = "0.2.0"
|
|
26
33
|
GENERATION = "1"
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
class ExitCode:
|
|
30
37
|
SUCCESS = 0
|
|
31
38
|
WARNING = 1
|
|
32
|
-
|
|
33
|
-
|
|
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="
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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,52 @@ 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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
268
|
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
|
-
|
|
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])
|
|
269
|
+
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
|
+
for capability in sorted(set(arguments.release_capability)): configuration = configuration.with_capability(supported[capability])
|
|
260
271
|
return configuration
|
|
261
272
|
|
|
262
273
|
|
|
263
274
|
def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
|
|
275
|
+
if arguments.command in {"diff"} or (arguments.command == "assess" and arguments.baseline):
|
|
276
|
+
return _differential_command(arguments, configuration, stream)
|
|
264
277
|
if arguments.command in {"baseline", "compare"}:
|
|
265
278
|
return _baseline_command(arguments, configuration, stream)
|
|
266
279
|
if arguments.command == "trend":
|
|
@@ -268,14 +281,7 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
|
|
|
268
281
|
if arguments.command == "query":
|
|
269
282
|
return _query_command(arguments, configuration, stream)
|
|
270
283
|
if arguments.command == "qualify":
|
|
271
|
-
|
|
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
|
|
284
|
+
return _qualification_command(arguments, configuration, stream)
|
|
279
285
|
if arguments.command == "report":
|
|
280
286
|
return _report_command(arguments, stream)
|
|
281
287
|
if arguments.command == "assure":
|
|
@@ -300,13 +306,13 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
|
|
|
300
306
|
return ExitCode.SUCCESS if evidence["decision"] == "RELEASE_CERTIFIED" else ExitCode.BLOCKED
|
|
301
307
|
if arguments.command in {"validate", "inspect", "assess", "run"}:
|
|
302
308
|
if arguments.command in {"assess", "run"}:
|
|
303
|
-
if not arguments.capability:
|
|
304
|
-
_render({"command": "assess", "status": "
|
|
309
|
+
if arguments.command == "run" and not arguments.capability:
|
|
310
|
+
_render({"command": "assess", "status": "NOT_SUPPORTED", "reason": "assess requires an explicit capability."}, arguments.format, stream)
|
|
305
311
|
return ExitCode.NOT_SUPPORTED
|
|
306
312
|
try:
|
|
307
313
|
store = EvidenceStore(_store_location(arguments, arguments.target)) if arguments.command in {"assess", "run"} else None
|
|
308
314
|
code, payload = _runtime_result(arguments.command, arguments.target, configuration, store)
|
|
309
|
-
except (ValueError, OSError, json.JSONDecodeError) as error:
|
|
315
|
+
except (PolicyError, ValueError, OSError, json.JSONDecodeError) as error:
|
|
310
316
|
_render({"status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
|
|
311
317
|
return ExitCode.BLOCKED
|
|
312
318
|
_render(payload, arguments.format, stream)
|
|
@@ -316,6 +322,61 @@ def _execute_command(arguments: argparse.Namespace, configuration: RuntimeConfig
|
|
|
316
322
|
return ExitCode.NOT_SUPPORTED
|
|
317
323
|
|
|
318
324
|
|
|
325
|
+
def _differential_command(arguments, configuration, stream):
|
|
326
|
+
location = configuration.execution_options.get("baseline", {}).get("location", ".tde/baselines")
|
|
327
|
+
registry = AssessmentBaselineRegistry(Path(arguments.target) / location if not Path(location).is_absolute() else location)
|
|
328
|
+
store = EvidenceStore(_store_location(arguments, arguments.target))
|
|
329
|
+
try:
|
|
330
|
+
if not arguments.baseline:
|
|
331
|
+
raise DifferentialError("diff requires --baseline")
|
|
332
|
+
current = Runtime().execute(arguments.target, configuration)
|
|
333
|
+
current_record = store.persist(current.evidence)
|
|
334
|
+
baseline = registry.load(arguments.baseline)
|
|
335
|
+
previous = store.retrieve(baseline["assessmentEvidenceId"].removeprefix("sha256:"))["evidence"]
|
|
336
|
+
differential = DifferentialEngine().compare(current.evidence, baseline, previous)
|
|
337
|
+
_render({"command": "diff", "baseline": baseline, "assessmentEvidenceStore": current_record,
|
|
338
|
+
"assessmentEvidence": current.evidence, "differentialEvidence": differential}, arguments.format, stream)
|
|
339
|
+
return _policy_exit_code(current.evidence["assessmentDecision"]["decision"])
|
|
340
|
+
except (DifferentialError, ValueError, OSError, json.JSONDecodeError) as error:
|
|
341
|
+
_render({"command": "diff", "status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
|
|
342
|
+
return ExitCode.BLOCKED
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _qualification_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
|
|
346
|
+
"""Run a profile-driven assessment and register its separate qualification result."""
|
|
347
|
+
try:
|
|
348
|
+
definition = RepositoryDefinitionRegistry().resolve(arguments.target, arguments.repository_definition)
|
|
349
|
+
profile = arguments.profile or definition["defaultAssessmentProfile"]
|
|
350
|
+
configuration = AssessmentOrchestrator().configure(
|
|
351
|
+
configuration, profile=profile,
|
|
352
|
+
capabilities=tuple(item.replace("-", "_") for item in arguments.capability),
|
|
353
|
+
)
|
|
354
|
+
target = str(definition["repositoryRoot"])
|
|
355
|
+
result = Runtime().execute(target, configuration)
|
|
356
|
+
qualification = RepositoryQualification.create(definition, result.evidence)
|
|
357
|
+
assessment_record = EvidenceStore(_store_location(arguments, target)).persist(result.evidence)
|
|
358
|
+
registry_record = QualificationRegistry(_qualification_location(arguments, target)).persist(qualification)
|
|
359
|
+
payload = {"command": "qualify", "repositoryQualification": qualification,
|
|
360
|
+
"qualificationRegistry": registry_record, "assessmentEvidence": result.evidence,
|
|
361
|
+
"assessmentEvidenceStore": assessment_record}
|
|
362
|
+
statuses = {item.get("status") for item in result.evidence.get("capabilityResults", [])}
|
|
363
|
+
if "NOT_SUPPORTED" in statuses:
|
|
364
|
+
code = ExitCode.NOT_SUPPORTED
|
|
365
|
+
elif "ANALYZER_NOT_FOUND" in statuses:
|
|
366
|
+
code = ExitCode.ANALYZER_NOT_FOUND
|
|
367
|
+
elif "FAILED_CLOSED" in statuses:
|
|
368
|
+
code = ExitCode.FAILED_CLOSED
|
|
369
|
+
elif qualification["qualificationStatus"] == "BLOCKED":
|
|
370
|
+
code = ExitCode.EXECUTION_ERROR
|
|
371
|
+
else:
|
|
372
|
+
code = _policy_exit_code(qualification["assessmentDecision"])
|
|
373
|
+
_render(payload, arguments.format, stream)
|
|
374
|
+
return code
|
|
375
|
+
except (RepositoryQualificationError, PolicyError, ValueError, OSError, json.JSONDecodeError) as error:
|
|
376
|
+
_render({"command": "qualify", "status": "BLOCKED", "reason": str(error)}, arguments.format, stream)
|
|
377
|
+
return ExitCode.BLOCKED
|
|
378
|
+
|
|
379
|
+
|
|
319
380
|
def _baseline_command(arguments: argparse.Namespace, configuration: RuntimeConfiguration, stream: TextIO) -> int:
|
|
320
381
|
location = configuration.execution_options.get("baseline", {}).get("location", ".tde/baselines")
|
|
321
382
|
repository = BaselineRepository(Path(arguments.target) / location if not Path(location).is_absolute() else location)
|
|
@@ -332,7 +393,7 @@ def _baseline_command(arguments: argparse.Namespace, configuration: RuntimeConfi
|
|
|
332
393
|
|
|
333
394
|
|
|
334
395
|
def _create_baseline(arguments, repository, evidence, persisted, stream):
|
|
335
|
-
baseline = repository.create(evidence, arguments.name)
|
|
396
|
+
baseline = AssessmentBaselineRegistry(repository.location).create(evidence, arguments.name)
|
|
336
397
|
_render({"command": "baseline", "status": "VALID", "evidenceStore": persisted, "baseline": baseline}, arguments.format, stream)
|
|
337
398
|
return ExitCode.SUCCESS
|
|
338
399
|
|
|
@@ -340,6 +401,9 @@ def _create_baseline(arguments, repository, evidence, persisted, stream):
|
|
|
340
401
|
def _compare_baseline(arguments, repository, current, evidence, persisted, stream):
|
|
341
402
|
if not arguments.baseline: raise BaselineError("compare requires --baseline")
|
|
342
403
|
baseline = repository.load(arguments.baseline)
|
|
404
|
+
if "evidence" not in baseline:
|
|
405
|
+
baseline = {**baseline, "evidence": EvidenceStore(_store_location(arguments, arguments.target)).retrieve(
|
|
406
|
+
baseline["assessmentEvidenceId"].removeprefix("sha256:"))["evidence"]}
|
|
343
407
|
comparison = ComparisonEngine().compare(evidence, baseline)
|
|
344
408
|
policy = PolicyEngine().load(current.context.configuration, current.context.repository_root, current.context.runtime_version, current.context.schema_version)
|
|
345
409
|
normalized = {"measurements": evidence["measurements"], "findings": evidence["findings"], "capabilityResults": evidence["capabilityResults"], "comparison": comparison}
|
|
@@ -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)
|
|
51
|
-
raise BaselineError("baseline
|
|
52
|
-
|
|
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,10 +1,11 @@
|
|
|
1
1
|
"""Code Size capability adapter backed by an explicitly installed cloc executable."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
import json,
|
|
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"
|
|
@@ -22,19 +23,15 @@ def classify(path: str) -> str:
|
|
|
22
23
|
return "SOURCE"
|
|
23
24
|
|
|
24
25
|
def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
|
|
25
|
-
|
|
26
|
-
if
|
|
27
|
-
return {"status":"
|
|
26
|
+
discovery = discover("cloc", MINIMUM_ANALYZER_VERSION, timeout)
|
|
27
|
+
if discovery["status"] != "VALID":
|
|
28
|
+
return {"status": discovery["status"], "limitations": [discovery["limitation"]]}
|
|
28
29
|
try:
|
|
29
|
-
|
|
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)
|
|
30
|
+
result = subprocess.run([discovery["executable"], "--json", "--by-file", "--quiet", str(root)], capture_output=True, text=True, timeout=timeout, check=True)
|
|
34
31
|
raw = result.stdout
|
|
35
32
|
data = json.loads(raw)
|
|
36
33
|
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, json.JSONDecodeError) as error:
|
|
37
|
-
return {"status":"
|
|
34
|
+
return {"status":"FAILED_CLOSED", "limitations":[{"id":"analyzer.cloc.failed","description":str(error),"cause":"analyzer execution failed"}]}
|
|
38
35
|
files, languages = [], defaultdict(lambda: {"files":0,"code":0,"comment":0,"blank":0})
|
|
39
36
|
totals = defaultdict(int)
|
|
40
37
|
entries = ((name, item) for name, item in data.items() if name not in {"header", "SUM"})
|
|
@@ -46,4 +43,4 @@ def analyze(root: Path, timeout: int = 60) -> dict[str, Any]:
|
|
|
46
43
|
for key in ("code","comment","blank"): totals[key] += item[key]
|
|
47
44
|
totals["files"] += 1; totals[category.lower()] += item["code"]
|
|
48
45
|
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"}]}
|
|
46
|
+
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
|
-
|
|
63
|
-
if
|
|
64
|
-
return {"status":"
|
|
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
|
-
|
|
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}
|
|
@@ -56,7 +56,7 @@ class RuntimeConfiguration:
|
|
|
56
56
|
@classmethod
|
|
57
57
|
def load(cls, values: Mapping[str, Any] | None = None) -> "RuntimeConfiguration":
|
|
58
58
|
values = {} if values is None else dict(values)
|
|
59
|
-
allowed = {"schemaVersion", "executionOptions", "capabilities", "policy", "baseline", "trend"}
|
|
59
|
+
allowed = {"schemaVersion", "executionOptions", "capabilities", "policy", "baseline", "trend", "assessment"}
|
|
60
60
|
unknown = set(values) - allowed
|
|
61
61
|
if unknown:
|
|
62
62
|
raise ValueError(f"unsupported runtime configuration: {sorted(unknown)}")
|
|
@@ -67,7 +67,7 @@ class RuntimeConfiguration:
|
|
|
67
67
|
if not isinstance(options, dict):
|
|
68
68
|
raise ValueError("executionOptions must be an object")
|
|
69
69
|
resolved: dict[str, Any] = dict(options)
|
|
70
|
-
for key in ("capabilities", "policy", "baseline", "trend"):
|
|
70
|
+
for key in ("capabilities", "policy", "baseline", "trend", "assessment"):
|
|
71
71
|
value = values.get(key, resolved.get(key, {}))
|
|
72
72
|
if not isinstance(value, dict):
|
|
73
73
|
raise ValueError(f"{key} must be an object")
|
|
@@ -104,6 +104,23 @@ class RuntimeConfiguration:
|
|
|
104
104
|
values["executionOptions"].pop("capabilities", None)
|
|
105
105
|
return self.load(values)
|
|
106
106
|
|
|
107
|
+
def with_assessment_profile(self, identifier: str, capabilities: tuple[str, ...], *,
|
|
108
|
+
identity: Mapping[str, Any] | None = None,
|
|
109
|
+
policy_file: str | None = None) -> "RuntimeConfiguration":
|
|
110
|
+
values = self.as_dict()
|
|
111
|
+
assessment = {"profile": identifier, "capabilities": list(capabilities)}
|
|
112
|
+
if identity:
|
|
113
|
+
assessment["profileIdentity"] = dict(identity)
|
|
114
|
+
values["assessment"] = assessment
|
|
115
|
+
values["executionOptions"].pop("assessment", None)
|
|
116
|
+
if policy_file and not values["executionOptions"].get("policy", {}).get("file"):
|
|
117
|
+
values["policy"] = {**values["executionOptions"].get("policy", {}), "file": policy_file}
|
|
118
|
+
values["executionOptions"].pop("policy", None)
|
|
119
|
+
configured = self.load(values)
|
|
120
|
+
for capability in capabilities:
|
|
121
|
+
configured = configured.with_capability(capability)
|
|
122
|
+
return configured
|
|
123
|
+
|
|
107
124
|
def as_dict(self) -> dict[str, Any]:
|
|
108
125
|
return {"schemaVersion": self.schema_version, "executionOptions": self.execution_options or {}}
|
|
109
126
|
|