codedd-cli 0.1.5__tar.gz → 0.1.6__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.
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/PKG-INFO +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/__init__.py +3 -3
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/api/client.py +2 -5
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/api/endpoints.py +9 -2
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/api/exceptions.py +24 -24
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/archetype_classifier.py +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/architecture_analyzer.py +35 -17
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/architecture_prompts.py +6 -2
- codedd_cli-0.1.6/codedd_cli/auditor/architecture_schema_contract.py +150 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/commit_classifier.py +2 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/complexity_analyzer.py +9 -4
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/dependency_scanner.py +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/enhanced_architecture_detectors.py +387 -381
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/file_auditor.py +30 -10
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/git_stats_collector.py +11 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/response_parser.py +484 -484
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auth/session.py +9 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auth/token_manager.py +6 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/__init__.py +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/ai_docs_cmd.py +1115 -1000
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/audit_cmd.py +24 -59
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/audits_cmd.py +37 -6
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/auth_cmd.py +7 -21
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/config_cmd.py +22 -25
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/fix_cmd.py +1173 -72
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/commands/scope_cmd.py +52 -29
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/config/constants.py +3 -2
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/config/fix_session.py +47 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/config/settings.py +115 -15
- codedd_cli-0.1.6/codedd_cli/config/url_validation.py +139 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/models/audit.py +11 -11
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/scanner/file_classifier.py +7 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/scanner/file_walker.py +28 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/scanner/line_counter.py +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/__init__.py +1 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/directory_validator.py +11 -1
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/display.py +64 -36
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/payload_inspector.py +1 -1
- codedd_cli-0.1.6/codedd_cli/utils/safe_path.py +51 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/validators.py +6 -7
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/pyproject.toml +67 -67
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/LICENSE +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/README.md +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/__main__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/api/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/audit_checkpoint.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/sub_audit_runner.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/tier_definitions.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auditor/vulnerability_validator.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/auth/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/cli.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/config/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/llm/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/llm/key_manager.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/models/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/models/account.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/models/local_directory.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/scanner/__init__.py +0 -0
- {codedd_cli-0.1.5 → codedd_cli-0.1.6}/codedd_cli/utils/security.py +0 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"""CodeDD CLI — run code audits from your terminal."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.1.
|
|
1
|
+
"""CodeDD CLI — run code audits from your terminal."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.6"
|
|
@@ -24,6 +24,7 @@ from codedd_cli.config.constants import (
|
|
|
24
24
|
USER_AGENT_PREFIX,
|
|
25
25
|
)
|
|
26
26
|
from codedd_cli.config.settings import ConfigManager
|
|
27
|
+
from codedd_cli.config.url_validation import should_verify_tls_for_api_url
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class CodeDDClient:
|
|
@@ -42,11 +43,7 @@ class CodeDDClient:
|
|
|
42
43
|
self._base_url = self._config.api_url.rstrip("/")
|
|
43
44
|
self._token = token if token is not None else TokenManager.retrieve()
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
# For HTTPS, always verify certificates
|
|
47
|
-
verify_tls = not self._base_url.startswith("http://localhost") and not self._base_url.startswith(
|
|
48
|
-
"http://127.0.0.1"
|
|
49
|
-
)
|
|
46
|
+
verify_tls = should_verify_tls_for_api_url(self._base_url)
|
|
50
47
|
|
|
51
48
|
self._client = httpx.Client(
|
|
52
49
|
base_url=self._base_url,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Server endpoint path constants.
|
|
3
3
|
|
|
4
|
-
All paths are
|
|
4
|
+
All paths are absolute from the server host root (they include the ``/api/``
|
|
5
|
+
prefix). The ``api_url`` in CLI config is the host only, e.g.
|
|
6
|
+
``https://api.codedd.ai`` or ``http://localhost:8000``.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
|
|
@@ -17,7 +19,8 @@ class Endpoints:
|
|
|
17
19
|
# Scope
|
|
18
20
|
REGISTER_SCOPE = "/api/cli/scope/register/"
|
|
19
21
|
SCOPE_FILES = "/api/cli/scope/files/"
|
|
20
|
-
|
|
22
|
+
# Legacy web route reused by the CLI (django_codedd.urls → api/delete_repo_from_scope/).
|
|
23
|
+
DELETE_REPO_FROM_SCOPE = "/api/delete_repo_from_scope/"
|
|
21
24
|
|
|
22
25
|
# Audit lifecycle
|
|
23
26
|
AUDIT_CAN_START = "/api/cli/audit/can-start/"
|
|
@@ -61,3 +64,7 @@ class Endpoints:
|
|
|
61
64
|
# Fix access control (remediation module)
|
|
62
65
|
FIX_ACCESS_STATUS = "/api/cli/fix/access/status/"
|
|
63
66
|
FIX_ACCESS_TOGGLE = "/api/cli/fix/access/toggle/"
|
|
67
|
+
|
|
68
|
+
# Issue Compass saved selections (flag-issue slices from the web explorer)
|
|
69
|
+
FIX_ISSUE_COMPASS_SELECTIONS = "/api/cli/fix/issue-compass-selections/"
|
|
70
|
+
FIX_ISSUE_COMPASS_SELECTIONS_CATALOG = "/api/cli/fix/issue-compass-selections/catalog/"
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"""
|
|
2
|
-
API-layer exceptions with user-facing messages.
|
|
3
|
-
|
|
4
|
-
Raised when the CLI cannot reach the CodeDD server (connection refused,
|
|
5
|
-
timeout, server disconnected, etc.). Handled at the CLI entry point to
|
|
6
|
-
display a single, clear message instead of a full traceback.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
# Pre-defined message shown when the remote is not responding.
|
|
10
|
-
CONNECTION_ERROR_MESSAGE = (
|
|
11
|
-
"Unable to reach CodeDD. "
|
|
12
|
-
"Check that the server is running and your network connection is available, "
|
|
13
|
-
"or try again later."
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class CodeDDConnectionError(Exception):
|
|
18
|
-
"""
|
|
19
|
-
Raised when a request to the CodeDD API fails due to connection
|
|
20
|
-
or transport errors (e.g. server not responding, timeout, disconnect).
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, message: str | None = None) -> None:
|
|
24
|
-
super().__init__(message or CONNECTION_ERROR_MESSAGE)
|
|
1
|
+
"""
|
|
2
|
+
API-layer exceptions with user-facing messages.
|
|
3
|
+
|
|
4
|
+
Raised when the CLI cannot reach the CodeDD server (connection refused,
|
|
5
|
+
timeout, server disconnected, etc.). Handled at the CLI entry point to
|
|
6
|
+
display a single, clear message instead of a full traceback.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Pre-defined message shown when the remote is not responding.
|
|
10
|
+
CONNECTION_ERROR_MESSAGE = (
|
|
11
|
+
"Unable to reach CodeDD. "
|
|
12
|
+
"Check that the server is running and your network connection is available, "
|
|
13
|
+
"or try again later."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CodeDDConnectionError(Exception):
|
|
18
|
+
"""
|
|
19
|
+
Raised when a request to the CodeDD API fails due to connection
|
|
20
|
+
or transport errors (e.g. server not responding, timeout, disconnect).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str | None = None) -> None:
|
|
24
|
+
super().__init__(message or CONNECTION_ERROR_MESSAGE)
|
|
@@ -159,7 +159,7 @@ class DimensionScorer:
|
|
|
159
159
|
has already been credited in another dimension."""
|
|
160
160
|
credited_dims = self._tech_credit_ledger.get(tech_keyword, set())
|
|
161
161
|
if dimension in credited_dims:
|
|
162
|
-
return base_points
|
|
162
|
+
return base_points * ATTENUATION_FACTOR
|
|
163
163
|
if credited_dims:
|
|
164
164
|
self._tech_credit_ledger.setdefault(tech_keyword, set()).add(dimension)
|
|
165
165
|
return base_points * ATTENUATION_FACTOR
|
|
@@ -24,10 +24,15 @@ from typing import Any
|
|
|
24
24
|
|
|
25
25
|
from codedd_cli.auditor.archetype_classifier import classify_local
|
|
26
26
|
from codedd_cli.auditor.architecture_prompts import CATEGORY_PROMPTS, CATEGORY_SCHEMAS
|
|
27
|
+
from codedd_cli.auditor.architecture_schema_contract import (
|
|
28
|
+
ensure_schema_registry_valid,
|
|
29
|
+
normalize_llm_category_payload,
|
|
30
|
+
)
|
|
27
31
|
from codedd_cli.auditor.enhanced_architecture_detectors import (
|
|
28
32
|
LocalEnhancedRelationshipDetector,
|
|
29
33
|
LocalEnhancedTechnologyDetector,
|
|
30
34
|
)
|
|
35
|
+
from codedd_cli.utils.safe_path import resolve_path_within_base
|
|
31
36
|
|
|
32
37
|
logger = logging.getLogger(__name__)
|
|
33
38
|
|
|
@@ -551,8 +556,8 @@ def _identify_technologies(
|
|
|
551
556
|
if prefix and dep_path.startswith(prefix)
|
|
552
557
|
else _get_relative_path(dep_path)
|
|
553
558
|
)
|
|
554
|
-
local_path =
|
|
555
|
-
if not os.path.isfile(local_path):
|
|
559
|
+
local_path = resolve_path_within_base(local_base, rel)
|
|
560
|
+
if not local_path or not os.path.isfile(local_path):
|
|
556
561
|
continue
|
|
557
562
|
file_name = os.path.basename(local_path)
|
|
558
563
|
try:
|
|
@@ -837,9 +842,26 @@ def _build_phase2_skeleton(
|
|
|
837
842
|
}
|
|
838
843
|
|
|
839
844
|
|
|
840
|
-
def _read_file_safely_local(
|
|
845
|
+
def _read_file_safely_local(
|
|
846
|
+
local_path: str,
|
|
847
|
+
max_chars: int = _MAX_CONTENT_CHARS,
|
|
848
|
+
*,
|
|
849
|
+
local_base: str | None = None,
|
|
850
|
+
) -> str | None:
|
|
841
851
|
"""Read file content with size limit; truncate by character count. Returns None on error."""
|
|
842
852
|
try:
|
|
853
|
+
if local_base:
|
|
854
|
+
try:
|
|
855
|
+
base_real = os.path.realpath(local_base)
|
|
856
|
+
path_real = os.path.realpath(local_path)
|
|
857
|
+
except OSError:
|
|
858
|
+
return None
|
|
859
|
+
try:
|
|
860
|
+
if os.path.commonpath([base_real, path_real]) != base_real:
|
|
861
|
+
return None
|
|
862
|
+
except ValueError:
|
|
863
|
+
return None
|
|
864
|
+
local_path = path_real
|
|
843
865
|
if not os.path.isfile(local_path):
|
|
844
866
|
return None
|
|
845
867
|
size = os.path.getsize(local_path)
|
|
@@ -902,17 +924,9 @@ def _parse_llm_response(response_text: str, file_path: str, category: str) -> di
|
|
|
902
924
|
return None
|
|
903
925
|
if not isinstance(data, dict):
|
|
904
926
|
return None
|
|
927
|
+
ensure_schema_registry_valid(CATEGORY_SCHEMAS, CATEGORY_PROMPTS)
|
|
905
928
|
schema = CATEGORY_SCHEMAS.get(category, CATEGORY_SCHEMAS["config_files"])
|
|
906
|
-
|
|
907
|
-
if key not in data:
|
|
908
|
-
data[key] = schema[key] if isinstance(schema[key], (list, dict)) else ""
|
|
909
|
-
else:
|
|
910
|
-
expected_type = type(schema[key])
|
|
911
|
-
if not isinstance(data[key], expected_type):
|
|
912
|
-
logger.warning(
|
|
913
|
-
"Schema drift detected for category=%r key=%r: expected %s, got %s (file=%s)",
|
|
914
|
-
category, key, expected_type.__name__, type(data[key]).__name__, file_path,
|
|
915
|
-
)
|
|
929
|
+
data = normalize_llm_category_payload(data, category, schema)
|
|
916
930
|
data["file_path"] = file_path
|
|
917
931
|
data["analysis_category"] = category
|
|
918
932
|
data["analysis_timestamp"] = time.time()
|
|
@@ -1954,9 +1968,11 @@ def _analyze_single_file(
|
|
|
1954
1968
|
gemini_client: Any,
|
|
1955
1969
|
grok_client: Any,
|
|
1956
1970
|
on_debug: Callable[[str], None] | None,
|
|
1971
|
+
*,
|
|
1972
|
+
local_base: str | None = None,
|
|
1957
1973
|
) -> dict[str, Any] | None:
|
|
1958
1974
|
"""Read file, build prompt, call LLM, parse. Returns analysis dict or None."""
|
|
1959
|
-
content = _read_file_safely_local(local_path)
|
|
1975
|
+
content = _read_file_safely_local(local_path, local_base=local_base)
|
|
1960
1976
|
if not content:
|
|
1961
1977
|
return None
|
|
1962
1978
|
prompt = _build_analysis_prompt(file_path, content, category)
|
|
@@ -2088,8 +2104,8 @@ def run_phase2_with_llm(
|
|
|
2088
2104
|
if not file_path.startswith(prefix):
|
|
2089
2105
|
continue
|
|
2090
2106
|
rel = file_path[len(prefix):].lstrip("/")
|
|
2091
|
-
local_path =
|
|
2092
|
-
if os.path.isfile(local_path):
|
|
2107
|
+
local_path = resolve_path_within_base(local_base, rel)
|
|
2108
|
+
if local_path and os.path.isfile(local_path):
|
|
2093
2109
|
work_items.append((category, file_path, local_path))
|
|
2094
2110
|
|
|
2095
2111
|
total_files = len(work_items)
|
|
@@ -2119,7 +2135,9 @@ def run_phase2_with_llm(
|
|
|
2119
2135
|
grok_key,
|
|
2120
2136
|
)
|
|
2121
2137
|
try:
|
|
2122
|
-
result = _analyze_single_file(
|
|
2138
|
+
result = _analyze_single_file(
|
|
2139
|
+
fp, lp, cat, a_client, o_client, g_client, x_client, on_debug, local_base=local_base,
|
|
2140
|
+
)
|
|
2123
2141
|
with progress_lock:
|
|
2124
2142
|
done_counter[0] += 1
|
|
2125
2143
|
done = done_counter[0]
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Category-specific prompts and output schemas for CLI architecture Phase 2.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Schema contract (enforced at runtime — see ``architecture_schema_contract``):
|
|
5
|
+
- ``SERVER_PARITY_CATEGORIES`` (8): key sets mirror server ``LLMAnalyzer.category_schemas``.
|
|
6
|
+
- ``CLI_EXTENSION_CATEGORIES`` (4): CLI-only categories with the same validation rules.
|
|
7
|
+
- ``SCHEMA_CONTRACT_VERSION``: bump when shared keys change on either side.
|
|
8
|
+
|
|
9
|
+
Per-file LLM JSON is normalized in ``architecture_analyzer._parse_llm_response``.
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
# Output structure expected from the LLM per category (server-compatible)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime contract for architecture Phase 2 LLM category schemas.
|
|
3
|
+
|
|
4
|
+
The eight ``SERVER_PARITY_CATEGORIES`` mirror ``LLMAnalyzer.category_schemas`` on
|
|
5
|
+
the CodeDD server (django auditor). Four ``CLI_EXTENSION_CATEGORIES`` are
|
|
6
|
+
CLI-only enrichments; they use the same validation machinery but are not sent
|
|
7
|
+
through the legacy server per-file parser.
|
|
8
|
+
|
|
9
|
+
``SCHEMA_CONTRACT_VERSION`` must be bumped when either side changes shared keys.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
SCHEMA_CONTRACT_VERSION = "1.0.0"
|
|
22
|
+
|
|
23
|
+
# Categories whose key sets are kept in parity with the server LLMAnalyzer.
|
|
24
|
+
SERVER_PARITY_CATEGORIES = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"dependency_files",
|
|
27
|
+
"infrastructure_files",
|
|
28
|
+
"backend_files",
|
|
29
|
+
"frontend_files",
|
|
30
|
+
"database_files",
|
|
31
|
+
"ci_cd_files",
|
|
32
|
+
"testing_files",
|
|
33
|
+
"config_files",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Additional CLI architecture categories (validated locally; not server-parity).
|
|
38
|
+
CLI_EXTENSION_CATEGORIES = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"api_schema_files",
|
|
41
|
+
"monitoring_files",
|
|
42
|
+
"message_queue_files",
|
|
43
|
+
"security_auth_files",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ALL_ARCHITECTURE_CATEGORIES = SERVER_PARITY_CATEGORIES | CLI_EXTENSION_CATEGORIES
|
|
48
|
+
|
|
49
|
+
_registry_checked = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def category_schema_fingerprint(schema: dict[str, Any]) -> str:
|
|
53
|
+
"""Stable short fingerprint of a schema's top-level keys (order-independent)."""
|
|
54
|
+
payload = json.dumps(sorted(schema.keys()), separators=(",", ":"))
|
|
55
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_schema_registry(
|
|
59
|
+
category_schemas: dict[str, dict[str, Any]],
|
|
60
|
+
category_prompts: dict[str, str],
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""
|
|
63
|
+
Verify prompt/schema registry integrity.
|
|
64
|
+
|
|
65
|
+
Returns a list of human-readable errors (empty when valid).
|
|
66
|
+
"""
|
|
67
|
+
errors: list[str] = []
|
|
68
|
+
|
|
69
|
+
schema_cats = set(category_schemas)
|
|
70
|
+
prompt_cats = set(category_prompts)
|
|
71
|
+
|
|
72
|
+
missing_prompts = schema_cats - prompt_cats
|
|
73
|
+
missing_schemas = prompt_cats - schema_cats
|
|
74
|
+
if missing_prompts:
|
|
75
|
+
errors.append(f"CATEGORY_SCHEMAS without prompts: {sorted(missing_prompts)}")
|
|
76
|
+
if missing_schemas:
|
|
77
|
+
errors.append(f"CATEGORY_PROMPTS without schemas: {sorted(missing_schemas)}")
|
|
78
|
+
|
|
79
|
+
for category, schema in category_schemas.items():
|
|
80
|
+
if not isinstance(schema, dict) or not schema:
|
|
81
|
+
errors.append(f"Empty or invalid schema for category {category!r}")
|
|
82
|
+
continue
|
|
83
|
+
if "file_type" not in schema:
|
|
84
|
+
errors.append(f"Schema for {category!r} missing required file_type key")
|
|
85
|
+
|
|
86
|
+
unknown = schema_cats - ALL_ARCHITECTURE_CATEGORIES
|
|
87
|
+
if unknown:
|
|
88
|
+
errors.append(f"Unknown architecture categories: {sorted(unknown)}")
|
|
89
|
+
|
|
90
|
+
return errors
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def ensure_schema_registry_valid(
|
|
94
|
+
category_schemas: dict[str, dict[str, Any]],
|
|
95
|
+
category_prompts: dict[str, str],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Run ``validate_schema_registry`` once per process; log errors on failure."""
|
|
98
|
+
global _registry_checked
|
|
99
|
+
if _registry_checked:
|
|
100
|
+
return
|
|
101
|
+
_registry_checked = True
|
|
102
|
+
errors = validate_schema_registry(category_schemas, category_prompts)
|
|
103
|
+
if errors:
|
|
104
|
+
logger.error(
|
|
105
|
+
"Architecture schema registry failed contract %s: %s",
|
|
106
|
+
SCHEMA_CONTRACT_VERSION,
|
|
107
|
+
"; ".join(errors),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def normalize_llm_category_payload(
|
|
112
|
+
data: dict[str, Any],
|
|
113
|
+
category: str,
|
|
114
|
+
schema: dict[str, Any],
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Enforce schema shape: backfill missing keys and coerce wrong JSON types.
|
|
118
|
+
|
|
119
|
+
Unknown keys from the LLM are preserved so forward-compatible fields are not
|
|
120
|
+
dropped. Type coercion follows the template value in *schema*.
|
|
121
|
+
"""
|
|
122
|
+
normalized = dict(data)
|
|
123
|
+
for key, template in schema.items():
|
|
124
|
+
if key not in normalized:
|
|
125
|
+
normalized[key] = (
|
|
126
|
+
list(template) if isinstance(template, list)
|
|
127
|
+
else dict(template) if isinstance(template, dict)
|
|
128
|
+
else template
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
value = normalized[key]
|
|
132
|
+
if isinstance(template, list) and not isinstance(value, list):
|
|
133
|
+
logger.warning(
|
|
134
|
+
"Schema drift for category=%r key=%r: expected list, got %s",
|
|
135
|
+
category,
|
|
136
|
+
key,
|
|
137
|
+
type(value).__name__,
|
|
138
|
+
)
|
|
139
|
+
normalized[key] = []
|
|
140
|
+
elif isinstance(template, dict) and not isinstance(value, dict):
|
|
141
|
+
logger.warning(
|
|
142
|
+
"Schema drift for category=%r key=%r: expected dict, got %s",
|
|
143
|
+
category,
|
|
144
|
+
key,
|
|
145
|
+
type(value).__name__,
|
|
146
|
+
)
|
|
147
|
+
normalized[key] = {}
|
|
148
|
+
elif isinstance(template, str) and not isinstance(value, str):
|
|
149
|
+
normalized[key] = str(value) if value is not None else ""
|
|
150
|
+
return normalized
|
|
@@ -243,7 +243,8 @@ class CommitClassifier:
|
|
|
243
243
|
|
|
244
244
|
# === Tertiary: change-ratio heuristics ===
|
|
245
245
|
if total_added > 0:
|
|
246
|
-
|
|
246
|
+
deletion_ratio = total_deleted / total_added
|
|
247
|
+
if deletion_ratio > 0.7:
|
|
247
248
|
return {"type": "refactoring", "confidence": 0.6,
|
|
248
249
|
"method": "change_ratio", "keywords_matched": []}
|
|
249
250
|
if total_changes < 50:
|
|
@@ -41,6 +41,8 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
41
41
|
from dataclasses import dataclass
|
|
42
42
|
from typing import Any
|
|
43
43
|
|
|
44
|
+
from codedd_cli.utils.safe_path import resolve_path_within_base
|
|
45
|
+
|
|
44
46
|
logger = logging.getLogger(__name__)
|
|
45
47
|
|
|
46
48
|
|
|
@@ -2009,10 +2011,13 @@ class LocalComplexityAnalyzer:
|
|
|
2009
2011
|
error="Missing local directory or relative path",
|
|
2010
2012
|
)
|
|
2011
2013
|
|
|
2012
|
-
disk_path =
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2014
|
+
disk_path = resolve_path_within_base(local_dir, relative_path)
|
|
2015
|
+
if disk_path is None:
|
|
2016
|
+
return FileComplexityResult(
|
|
2017
|
+
file_path=file_path,
|
|
2018
|
+
relative_path=relative_path,
|
|
2019
|
+
error="Relative path escapes the local scope directory",
|
|
2020
|
+
)
|
|
2016
2021
|
|
|
2017
2022
|
try:
|
|
2018
2023
|
with open(disk_path, encoding="utf-8", errors="replace") as fh:
|
|
@@ -314,7 +314,7 @@ class ManifestParser:
|
|
|
314
314
|
raw_packages = dispatch_table[ext](file_content)
|
|
315
315
|
else:
|
|
316
316
|
for pattern, parser_func in dispatch_table.items():
|
|
317
|
-
if
|
|
317
|
+
if filename_lower == pattern.lower():
|
|
318
318
|
raw_packages = parser_func(file_content)
|
|
319
319
|
break
|
|
320
320
|
else:
|