technical-debt-engine-runtime 0.1.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/PKG-INFO +2 -1
- technical_debt_engine_runtime-1.0.0/README.md +44 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/pyproject.toml +4 -3
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/main.py +141 -76
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/__init__.py +2 -1
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/analyzer_discovery.py +21 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/assessment.py +33 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/baseline.py +6 -3
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/code_size.py +9 -11
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/complexity.py +6 -11
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/configuration.py +19 -2
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/coverage.py +210 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/dependency_health.py +173 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/differential.py +50 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/evidence_store.py +9 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/execution.py +98 -24
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/policies/generation-1.json +18 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policy.py +106 -8
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/profiles/__init__.py +1 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/profiles/minimal.json +10 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/profiles/standard.json +13 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/registries.py +110 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/repository_qualification.py +119 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime.py +100 -25
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/runtime_qualification.py +4 -2
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/__init__.py +88 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/assessment-decision-evidence.json +8 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/assessment-evidence.json +8 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/capability-evidence.json +8 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/common.schema.json +7 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/differential-evidence.json +1 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/policy-evidence.json +8 -0
- technical_debt_engine_runtime-1.0.0/src/tde_runtime/schemas/repository-qualification-evidence.json +8 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trend.py +3 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/PKG-INFO +2 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/SOURCES.txt +22 -0
- technical_debt_engine_runtime-1.0.0/src/technical_debt_engine_runtime.egg-info/requires.txt +1 -0
- technical_debt_engine_runtime-1.0.0/tests/test_capability_platform.py +21 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_cli.py +40 -8
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_code_size.py +35 -10
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_complexity.py +3 -3
- technical_debt_engine_runtime-1.0.0/tests/test_coverage.py +118 -0
- technical_debt_engine_runtime-1.0.0/tests/test_dependency_health.py +138 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_policy.py +31 -0
- technical_debt_engine_runtime-1.0.0/tests/test_public_artifacts.py +152 -0
- technical_debt_engine_runtime-1.0.0/tests/test_public_policy_configuration.py +215 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_bundle.py +16 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_publication.py +12 -7
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_qualification.py +10 -1
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime.py +36 -8
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_runtime_qualification.py +1 -1
- technical_debt_engine_runtime-0.1.0/README.md +0 -30
- technical_debt_engine_runtime-0.1.0/src/tde_runtime/dependency_health.py +0 -23
- 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-1.0.0}/setup.cfg +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_cli/__init__.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/maintainability.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/models.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/policies/__init__.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/query.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_bundle.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_certification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_publication.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/release_qualification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/tde_runtime/trusted_delivery.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/dependency_links.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/entry_points.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/src/technical_debt_engine_runtime.egg-info/top_level.txt +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_baseline.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_build_reproducibility.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_docker_artifact.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_evidence_store.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_execution.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_query.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_candidate.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_release_certification.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_software_assurance.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trend.py +0 -0
- {technical_debt_engine_runtime-0.1.0 → technical_debt_engine_runtime-1.0.0}/tests/test_trusted_delivery.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Technical Debt Engine
|
|
2
|
+
|
|
3
|
+
DJConnect is the primary product. Technical Debt Engine (TDE) is its compact,
|
|
4
|
+
supporting engineering tool for producing reliable pipeline assessment
|
|
5
|
+
decisions through public evidence and CLI contracts.
|
|
6
|
+
|
|
7
|
+
Generation 2 is the consumer-driven **TDE 1.0 DJConnect Enablement Program**.
|
|
8
|
+
It does not expand TDE into a general platform: its active scope is coverage
|
|
9
|
+
completion, minimal dependency and security evidence, selected DJConnect CI
|
|
10
|
+
consumption, and one integrated `1.0.0` qualification and release. See the
|
|
11
|
+
[roadmap](PRODUCT_ROADMAP.md) and [active backlog](PRODUCT_BACKLOG.md).
|
|
12
|
+
|
|
13
|
+
Release `0.2.0` is available as the PyPI distribution
|
|
14
|
+
[`technical-debt-engine-runtime`](https://pypi.org/project/technical-debt-engine-runtime/0.2.0/),
|
|
15
|
+
a [GitHub Release](https://github.com/pcvantol/technical-debt-engine/releases/tag/0.2.0),
|
|
16
|
+
and Docker image `docker.io/pcvantol/technical-debt-engine:0.2.0`. The Docker
|
|
17
|
+
OCI index is `sha256:8285a5082eaa1a5ac914b349ddec21c9e02cc4269421774d4f112383bc688ca9`;
|
|
18
|
+
no `latest` tag exists. See the [Runtime Qualification Report](RUNTIME_QUALIFICATION_REPORT_0.2.0.md)
|
|
19
|
+
for the immutable publication evidence and public-runtime validation.
|
|
20
|
+
|
|
21
|
+
## Product contracts
|
|
22
|
+
|
|
23
|
+
Consumers integrate only through the public `tde` CLI, configuration, evidence
|
|
24
|
+
schema, exit codes, and stable released contracts—not runtime internals. See
|
|
25
|
+
[INTEGRATION_MODEL.md](INTEGRATION_MODEL.md).
|
|
26
|
+
|
|
27
|
+
Operational repository assurance is available through `tde assure`; see [SOFTWARE_ASSURANCE.md](SOFTWARE_ASSURANCE.md) for its canonical evidence and candidate-artifact verification contract.
|
|
28
|
+
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
- [Product architecture](PRODUCT_ARCHITECTURE.md)
|
|
32
|
+
- [Platform vision](PLATFORM_VISION.md) and [platform strategy](PLATFORM_STRATEGY.md)
|
|
33
|
+
- [Engineering method](ENGINEERING_METHOD.md) and [session bootstrap](BOOTSTRAP.md)
|
|
34
|
+
- [Capability model](CAPABILITY_MODEL.md)
|
|
35
|
+
- [CLI specification](CLI_SPECIFICATION.md)
|
|
36
|
+
- [Code Size Runtime contract](CODE_SIZE_RUNTIME.md)
|
|
37
|
+
- [Evidence schema](EVIDENCE_SCHEMA.md)
|
|
38
|
+
- [Qualification model](QUALIFICATION_MODEL.md)
|
|
39
|
+
- [Roadmap](PRODUCT_ROADMAP.md) and [backlog](PRODUCT_BACKLOG.md)
|
|
40
|
+
- [Versioning](VERSIONING.md) and [release strategy](RELEASE_STRATEGY.md)
|
|
41
|
+
- [Package build reproducibility](PACKAGING.md)
|
|
42
|
+
- [Repository status](REPOSITORY_STATUS.md)
|
|
43
|
+
|
|
44
|
+
The documentation index is [PROMPT_INDEX.md](PROMPT_INDEX.md).
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==
|
|
2
|
+
requires = ["setuptools==83.0.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "technical-debt-engine-runtime"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.0.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-1.0.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 = "
|
|
32
|
+
CLI_VERSION = "1.0.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,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
|
-
|
|
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
|
-
supported = {"code-size": "code_size", "complexity": "complexity"
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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": "
|
|
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}
|
|
@@ -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,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,
|
|
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
|
-
|
|
26
|
-
if
|
|
27
|
-
return {"status":"
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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}
|