time-assurance-code-scanner 0.2.0rc2__py3-none-any.whl
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.
- config_detector/__init__.py +11 -0
- config_detector/analyzers/__init__.py +9 -0
- config_detector/analyzers/architecture.py +188 -0
- config_detector/analyzers/time_t_config.py +183 -0
- config_detector/build_parsers/__init__.py +10 -0
- config_detector/build_parsers/base.py +82 -0
- config_detector/build_parsers/cmake.py +113 -0
- config_detector/build_parsers/makefile.py +112 -0
- config_detector/cache.py +109 -0
- config_detector/cli.py +403 -0
- config_detector/detector.py +106 -0
- config_detector/hybrid_detector.py +384 -0
- config_detector/likelihoods.py +166 -0
- config_detector/llm_analyzer.py +368 -0
- config_detector/output/__init__.py +8 -0
- config_detector/output/likelihood.py +314 -0
- config_detector/validator.py +147 -0
- envui/__init__.py +4 -0
- envui/cli/__init__.py +4 -0
- envui/cli/derive_fields.py +147 -0
- envui/cli/env_wizard.py +381 -0
- envui/cli/io_utils.py +139 -0
- envui/cli/validate_env.py +156 -0
- envui/examples/env_config.sample.json +15 -0
- envui/schemas/environment.schema.json +151 -0
- report_renderer/__init__.py +8 -0
- report_renderer/__main__.py +8 -0
- report_renderer/cli.py +165 -0
- report_renderer/core.py +535 -0
- report_renderer/fixtures/malformed.json +20 -0
- report_renderer/fixtures/missing-fields.json +22 -0
- report_renderer/fixtures/typical.json +38 -0
- report_renderer/renderers.py +743 -0
- tacs/__init__.py +6 -0
- tacs/batch_render_reports.py +434 -0
- tacs/batch_repo_scan.py +390 -0
- tacs/batch_scan_repos.py +1556 -0
- tacs/cli.py +157 -0
- tacs/core/__init__.py +4 -0
- tacs/core/alias_scope.py +256 -0
- tacs/core/arithmetic_scanner.py +212 -0
- tacs/core/candidate_evidence.py +220 -0
- tacs/core/candidate_utils.py +238 -0
- tacs/core/config_validator.py +216 -0
- tacs/core/define_scanner.py +325 -0
- tacs/core/discovery_manager.py +389 -0
- tacs/core/env_capabilities.py +95 -0
- tacs/core/file_limits.py +68 -0
- tacs/core/function_analysis_response.py +438 -0
- tacs/core/function_analyzer.py +458 -0
- tacs/core/function_llm_client.py +1100 -0
- tacs/core/function_scanner.py +485 -0
- tacs/core/function_schemas.py +105 -0
- tacs/core/include_patterns.py +27 -0
- tacs/core/io_boundary_analyzer.py +706 -0
- tacs/core/io_format_parser.py +176 -0
- tacs/core/ir_adapter.py +465 -0
- tacs/core/ir_match.py +410 -0
- tacs/core/llm_client.py +2093 -0
- tacs/core/llm_errors.py +360 -0
- tacs/core/llm_logger.py +42 -0
- tacs/core/llm_prompt.py +45 -0
- tacs/core/llm_response.py +34 -0
- tacs/core/logging_config.py +128 -0
- tacs/core/metrics.py +92 -0
- tacs/core/migration_analyzer.py +373 -0
- tacs/core/model_assessment.py +368 -0
- tacs/core/path_utils.py +190 -0
- tacs/core/pipeline.py +3363 -0
- tacs/core/rule_catalog.py +206 -0
- tacs/core/run_ids.py +34 -0
- tacs/core/scan_session.py +761 -0
- tacs/core/schema.py +484 -0
- tacs/core/source_files.py +149 -0
- tacs/core/status_logger.py +97 -0
- tacs/core/struct_filter.py +108 -0
- tacs/core/typedef_scanner.py +246 -0
- tacs/fixtures/repo_lists/test_ilp32_repos.jsonl +7 -0
- tacs/fixtures/repo_lists/test_repos.jsonl +10 -0
- tacs/fixtures/repo_lists/test_time_t_size_repos.jsonl +7 -0
- tacs/llm/__init__.py +43 -0
- tacs/llm/env.py +166 -0
- tacs/llm/factory.py +29 -0
- tacs/llm/openai_models.py +187 -0
- tacs/python/y2038scan_fast_json_group.py +833 -0
- tacs/rules/retired_rule_ids.json +1 -0
- tacs/rules/y2038_sample_rules.json +6134 -0
- tacs/scan_command.py +288 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/METADATA +184 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/RECORD +94 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/WHEEL +5 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/entry_points.txt +2 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/licenses/LICENSE +203 -0
- time_assurance_code_scanner-0.2.0rc2.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright (c) 2026 Y2038.com LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Makefile parser for extracting compiler flags and configuration."""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Set
|
|
9
|
+
from .base import BaseParser
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MakefileParser(BaseParser):
|
|
13
|
+
"""Parser for Makefile build systems."""
|
|
14
|
+
|
|
15
|
+
# Common Makefile names
|
|
16
|
+
MAKEFILE_NAMES = ['Makefile', 'makefile', 'GNUmakefile', 'Makefile.in']
|
|
17
|
+
|
|
18
|
+
def find_build_files(self) -> List[Path]:
|
|
19
|
+
"""Find Makefiles in the project."""
|
|
20
|
+
build_files = []
|
|
21
|
+
|
|
22
|
+
# Check root directory first
|
|
23
|
+
for name in self.MAKEFILE_NAMES:
|
|
24
|
+
makefile = self.root_path / name
|
|
25
|
+
if makefile.exists():
|
|
26
|
+
build_files.append(makefile)
|
|
27
|
+
|
|
28
|
+
# Search for Makefiles in subdirectories (limit depth to avoid too many)
|
|
29
|
+
for makefile in self.root_path.rglob('Makefile'):
|
|
30
|
+
if makefile.is_file() and makefile.parent != self.root_path:
|
|
31
|
+
# Limit to common locations
|
|
32
|
+
rel_path = makefile.relative_to(self.root_path)
|
|
33
|
+
if any(part in ['src', 'lib', 'drivers', 'kernel', 'arch'] for part in rel_path.parts[:3]):
|
|
34
|
+
build_files.append(makefile)
|
|
35
|
+
|
|
36
|
+
return build_files[:10] # Limit to first 10 to avoid performance issues
|
|
37
|
+
|
|
38
|
+
def extract_flags(self, build_file: Path) -> Dict[str, any]:
|
|
39
|
+
"""
|
|
40
|
+
Extract compiler flags from a Makefile.
|
|
41
|
+
|
|
42
|
+
Looks for:
|
|
43
|
+
- CFLAGS, CPPFLAGS, LDFLAGS
|
|
44
|
+
- CC (compiler/toolchain)
|
|
45
|
+
-D defines
|
|
46
|
+
- ARCH, TARGET, BOARD variables
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
content = build_file.read_text(encoding='utf-8', errors='ignore')
|
|
50
|
+
except Exception:
|
|
51
|
+
return {'cflags': [], 'defines': [], 'variables': {}, 'toolchain': ''}
|
|
52
|
+
|
|
53
|
+
cflags: Set[str] = set()
|
|
54
|
+
defines: Set[str] = set()
|
|
55
|
+
variables: Dict[str, str] = {}
|
|
56
|
+
toolchain = ""
|
|
57
|
+
|
|
58
|
+
# Pattern for variable assignments: VAR = value or VAR := value or VAR += value
|
|
59
|
+
var_pattern = re.compile(r'^(\w+)\s*[+:]?=\s*(.+)$', re.MULTILINE)
|
|
60
|
+
|
|
61
|
+
# Pattern for -D defines
|
|
62
|
+
define_pattern = re.compile(r'-D(\w+(?:=\w+)?)')
|
|
63
|
+
|
|
64
|
+
# Extract variable assignments
|
|
65
|
+
for match in var_pattern.finditer(content):
|
|
66
|
+
var_name = match.group(1).upper()
|
|
67
|
+
var_value = match.group(2).strip()
|
|
68
|
+
|
|
69
|
+
# Remove comments
|
|
70
|
+
var_value = re.sub(r'#.*$', '', var_value).strip()
|
|
71
|
+
|
|
72
|
+
variables[var_name] = var_value
|
|
73
|
+
|
|
74
|
+
# Extract flags from common flag variables
|
|
75
|
+
if var_name in ['CFLAGS', 'CPPFLAGS', 'LDFLAGS', 'EXTRA_CFLAGS']:
|
|
76
|
+
# Extract individual flags
|
|
77
|
+
flags = var_value.split()
|
|
78
|
+
for flag in flags:
|
|
79
|
+
flag = flag.strip()
|
|
80
|
+
if flag:
|
|
81
|
+
cflags.add(flag)
|
|
82
|
+
|
|
83
|
+
# Extract -D defines
|
|
84
|
+
define_match = define_pattern.search(flag)
|
|
85
|
+
if define_match:
|
|
86
|
+
defines.add(define_match.group(1))
|
|
87
|
+
|
|
88
|
+
# Extract toolchain from CC
|
|
89
|
+
if var_name == 'CC' and not toolchain:
|
|
90
|
+
# Extract toolchain prefix (e.g., "arm-none-eabi-gcc" -> "arm-none-eabi")
|
|
91
|
+
toolchain_match = re.match(r'^([\w-]+)-gcc', var_value)
|
|
92
|
+
if toolchain_match:
|
|
93
|
+
toolchain = toolchain_match.group(1)
|
|
94
|
+
else:
|
|
95
|
+
toolchain = var_value
|
|
96
|
+
|
|
97
|
+
# Also look for inline flags in rules
|
|
98
|
+
inline_flag_pattern = re.compile(r'\$\((?:CFLAGS|CPPFLAGS)\)|(-[mD]\w+(?:=\w+)?)')
|
|
99
|
+
for match in inline_flag_pattern.finditer(content):
|
|
100
|
+
flag = match.group(0) if match.group(0) else match.group(1)
|
|
101
|
+
if flag and flag.startswith('-'):
|
|
102
|
+
cflags.add(flag)
|
|
103
|
+
define_match = define_pattern.search(flag)
|
|
104
|
+
if define_match:
|
|
105
|
+
defines.add(define_match.group(1))
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
'cflags': list(cflags),
|
|
109
|
+
'defines': list(defines),
|
|
110
|
+
'variables': variables,
|
|
111
|
+
'toolchain': toolchain
|
|
112
|
+
}
|
config_detector/cache.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright (c) 2026 Y2038.com LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Caching support for LLM analysis results."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigCache:
|
|
14
|
+
"""Cache for configuration detection results."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, cache_dir: Path = Path("results/config_cache"), cache_days: int = 30):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the cache.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
cache_dir: Directory to store cache files
|
|
22
|
+
cache_days: Number of days to keep cached results
|
|
23
|
+
"""
|
|
24
|
+
self.cache_dir = Path(cache_dir)
|
|
25
|
+
self.cache_days = cache_days
|
|
26
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
def get_cache_key(self, build_files: Dict[str, str], model: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Generate a cache key from build files and model name.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
build_files: Dictionary mapping filename to file content
|
|
34
|
+
model: LLM model name
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Cache key (hash string)
|
|
38
|
+
"""
|
|
39
|
+
# Create a deterministic hash from build files and model
|
|
40
|
+
content = json.dumps(build_files, sort_keys=True) + model
|
|
41
|
+
return hashlib.sha256(content.encode('utf-8')).hexdigest()[:16]
|
|
42
|
+
|
|
43
|
+
def get(self, cache_key: str) -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Get cached result if available and not expired.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
cache_key: Cache key
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Cached result or None if not found/expired
|
|
52
|
+
"""
|
|
53
|
+
cache_file = self.cache_dir / f"{cache_key}.json"
|
|
54
|
+
|
|
55
|
+
if not cache_file.exists():
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
60
|
+
cached = json.load(f)
|
|
61
|
+
|
|
62
|
+
# Check if expired
|
|
63
|
+
cached_time = datetime.fromisoformat(cached['timestamp'])
|
|
64
|
+
if datetime.now() - cached_time > timedelta(days=self.cache_days):
|
|
65
|
+
cache_file.unlink() # Delete expired cache
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return cached['result']
|
|
69
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
70
|
+
# Invalid cache file, delete it
|
|
71
|
+
cache_file.unlink()
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def set(self, cache_key: str, result: Dict[str, Any]):
|
|
75
|
+
"""
|
|
76
|
+
Store result in cache.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
cache_key: Cache key
|
|
80
|
+
result: Result to cache
|
|
81
|
+
"""
|
|
82
|
+
cache_file = self.cache_dir / f"{cache_key}.json"
|
|
83
|
+
|
|
84
|
+
cached = {
|
|
85
|
+
'timestamp': datetime.now().isoformat(),
|
|
86
|
+
'result': result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
|
90
|
+
json.dump(cached, f, indent=2)
|
|
91
|
+
|
|
92
|
+
def clear(self):
|
|
93
|
+
"""Clear all cache files."""
|
|
94
|
+
for cache_file in self.cache_dir.glob("*.json"):
|
|
95
|
+
cache_file.unlink()
|
|
96
|
+
|
|
97
|
+
def cleanup_expired(self):
|
|
98
|
+
"""Remove expired cache files."""
|
|
99
|
+
now = datetime.now()
|
|
100
|
+
for cache_file in self.cache_dir.glob("*.json"):
|
|
101
|
+
try:
|
|
102
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
103
|
+
cached = json.load(f)
|
|
104
|
+
cached_time = datetime.fromisoformat(cached['timestamp'])
|
|
105
|
+
if now - cached_time > timedelta(days=self.cache_days):
|
|
106
|
+
cache_file.unlink()
|
|
107
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
108
|
+
# Invalid cache file, delete it
|
|
109
|
+
cache_file.unlink()
|
config_detector/cli.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2026 Y2038.com LLC
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""CLI interface for the configuration detector."""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Handle both module and direct script execution
|
|
13
|
+
try:
|
|
14
|
+
from .detector import ConfigDetector
|
|
15
|
+
from .hybrid_detector import HybridConfigDetector
|
|
16
|
+
except ImportError:
|
|
17
|
+
# If running as script directly, add parent to path
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
from config_detector.detector import ConfigDetector
|
|
20
|
+
from config_detector.hybrid_detector import HybridConfigDetector
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
"""Main CLI entry point."""
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
description='Auto-detect environment configuration likelihoods from build systems',
|
|
27
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
28
|
+
epilog="""
|
|
29
|
+
Examples:
|
|
30
|
+
# Basic usage (human-readable output, keyword-based only)
|
|
31
|
+
python -m config_detector.cli /path/to/project
|
|
32
|
+
|
|
33
|
+
# JSON output
|
|
34
|
+
python -m config_detector.cli /path/to/project --format json
|
|
35
|
+
|
|
36
|
+
# Enable LLM analysis (hybrid approach)
|
|
37
|
+
python -m config_detector.cli /path/to/project --llm ollama --model qwen3-coder:480b-cloud
|
|
38
|
+
|
|
39
|
+
# Force LLM analysis even if confidence is high
|
|
40
|
+
python -m config_detector.cli /path/to/project --llm ollama --force-llm
|
|
41
|
+
|
|
42
|
+
# Keyword-only (no LLM)
|
|
43
|
+
python -m config_detector.cli /path/to/project --keyword-only
|
|
44
|
+
|
|
45
|
+
# Non-interactive mode (auto-accept if high confidence)
|
|
46
|
+
python -m config_detector.cli /path/to/project --llm ollama --non-interactive
|
|
47
|
+
|
|
48
|
+
# Save to file
|
|
49
|
+
python -m config_detector.cli /path/to/project --out results/config_likelihoods.json
|
|
50
|
+
|
|
51
|
+
# Verbose output (show all evidence)
|
|
52
|
+
python -m config_detector.cli /path/to/project --verbose
|
|
53
|
+
"""
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
'project_path',
|
|
58
|
+
type=str,
|
|
59
|
+
help='Path to the project root directory'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
'--format',
|
|
64
|
+
choices=['human', 'json'],
|
|
65
|
+
default='human',
|
|
66
|
+
help='Output format (default: human)'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
'--out',
|
|
71
|
+
type=str,
|
|
72
|
+
help='Output file path (if not specified, prints to stdout)'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
'--verbose',
|
|
77
|
+
action='store_true',
|
|
78
|
+
help='Show detailed evidence and reasoning'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# LLM options
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
'--llm',
|
|
84
|
+
choices=['none', 'ollama'],
|
|
85
|
+
default='none',
|
|
86
|
+
help='LLM type to use for analysis (default: none, keyword-based only)'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
'--model',
|
|
91
|
+
type=str,
|
|
92
|
+
default='gpt-oss:120b-cloud',
|
|
93
|
+
help='LLM model name (default: gpt-oss:120b-cloud)'
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
'--llm-timeout',
|
|
98
|
+
type=int,
|
|
99
|
+
default=60,
|
|
100
|
+
help='LLM request timeout in seconds (default: 60)'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
'--confidence-threshold',
|
|
105
|
+
type=float,
|
|
106
|
+
default=0.7,
|
|
107
|
+
help='Confidence threshold for triggering LLM analysis (default: 0.7)'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
'--force-llm',
|
|
112
|
+
action='store_true',
|
|
113
|
+
help='Force LLM analysis even if confidence is high'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
'--keyword-only',
|
|
118
|
+
action='store_true',
|
|
119
|
+
help='Only use keyword-based detection (no LLM)'
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
'--non-interactive',
|
|
124
|
+
action='store_true',
|
|
125
|
+
help='Non-interactive mode: auto-accept if high confidence, abort if low'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
'--no-cache',
|
|
130
|
+
action='store_true',
|
|
131
|
+
help='Disable caching of LLM results'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
'--debug',
|
|
136
|
+
action='store_true',
|
|
137
|
+
help='Enable safe diagnostic output (sizes/hashes; no raw prompts/responses)'
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
'--debug-llm-raw',
|
|
142
|
+
action='store_true',
|
|
143
|
+
help=(
|
|
144
|
+
'Dump raw LLM prompts/responses (privacy-sensitive; may include '
|
|
145
|
+
'source-derived material). Compatibility: --debug no longer dumps raw content.'
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
|
|
151
|
+
# Validate project path
|
|
152
|
+
project_path = Path(args.project_path)
|
|
153
|
+
if not project_path.exists():
|
|
154
|
+
print(f"Error: Project path does not exist: {project_path}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
if not project_path.is_dir():
|
|
158
|
+
print(f"Error: Project path is not a directory: {project_path}", file=sys.stderr)
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Determine which detector to use
|
|
163
|
+
use_hybrid = args.llm != 'none' and not args.keyword_only
|
|
164
|
+
|
|
165
|
+
if use_hybrid:
|
|
166
|
+
# Use hybrid detector
|
|
167
|
+
detector = HybridConfigDetector(
|
|
168
|
+
root_path=project_path,
|
|
169
|
+
llm_type=args.llm,
|
|
170
|
+
llm_model=args.model,
|
|
171
|
+
llm_timeout=args.llm_timeout,
|
|
172
|
+
confidence_threshold=args.confidence_threshold,
|
|
173
|
+
use_cache=not args.no_cache,
|
|
174
|
+
debug=args.debug or args.verbose,
|
|
175
|
+
debug_llm_raw=args.debug_llm_raw,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Run hybrid detection
|
|
179
|
+
results = detector.detect(
|
|
180
|
+
force_llm=args.force_llm,
|
|
181
|
+
keyword_only=args.keyword_only
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Handle user review if interactive
|
|
185
|
+
if not args.non_interactive and results.get('llm_results'):
|
|
186
|
+
results = _review_results(results, args)
|
|
187
|
+
|
|
188
|
+
# Format output
|
|
189
|
+
output = _format_hybrid_results(results, project_path, args.format, args.verbose)
|
|
190
|
+
else:
|
|
191
|
+
# Use keyword-based detector only
|
|
192
|
+
detector = ConfigDetector(project_path)
|
|
193
|
+
results = detector.detect()
|
|
194
|
+
output = detector.format_results(results, format_type=args.format)
|
|
195
|
+
|
|
196
|
+
# Output results
|
|
197
|
+
if args.out:
|
|
198
|
+
output_path = Path(args.out)
|
|
199
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
|
|
201
|
+
# If we have a final_config, save it as JSON (scanner expects pure JSON config)
|
|
202
|
+
# Otherwise, save the formatted output
|
|
203
|
+
final_config = results.get('final_config')
|
|
204
|
+
if final_config:
|
|
205
|
+
# Save the final_config as JSON (what the scanner expects)
|
|
206
|
+
output_path.write_text(json.dumps(final_config, indent=2), encoding='utf-8')
|
|
207
|
+
print(f"Configuration saved to: {output_path}")
|
|
208
|
+
elif args.format == 'json':
|
|
209
|
+
# Save full results as JSON
|
|
210
|
+
output_path.write_text(output, encoding='utf-8')
|
|
211
|
+
print(f"Results saved to: {output_path}")
|
|
212
|
+
else:
|
|
213
|
+
# Save human-readable format
|
|
214
|
+
output_path.write_text(output, encoding='utf-8')
|
|
215
|
+
print(f"Results saved to: {output_path}")
|
|
216
|
+
else:
|
|
217
|
+
print(output)
|
|
218
|
+
|
|
219
|
+
# Exit code based on whether detection was successful
|
|
220
|
+
if use_hybrid:
|
|
221
|
+
build_files_found = len(detector._collect_build_files()) if hasattr(detector, '_collect_build_files') else 0
|
|
222
|
+
else:
|
|
223
|
+
build_files_found = results.get('extracted_info', {}).get('build_files_found', 0)
|
|
224
|
+
|
|
225
|
+
if build_files_found == 0:
|
|
226
|
+
print("\nWarning: No build files found. Detection may be inaccurate.", file=sys.stderr)
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
sys.exit(0)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
233
|
+
if args.verbose or args.debug:
|
|
234
|
+
import traceback
|
|
235
|
+
traceback.print_exc()
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _review_results(results: dict, args: argparse.Namespace) -> dict:
|
|
240
|
+
"""Interactive review of detection results."""
|
|
241
|
+
print("\n" + "="*70)
|
|
242
|
+
print("PHASE 1: Keyword-Based Configuration Detection")
|
|
243
|
+
print("="*70)
|
|
244
|
+
|
|
245
|
+
keyword_results = results.get('keyword_results', {})
|
|
246
|
+
confidence = results.get('confidence', {})
|
|
247
|
+
|
|
248
|
+
# Display keyword-based results
|
|
249
|
+
print("\nDetected Configuration (Keyword-Based):")
|
|
250
|
+
_display_config_summary(keyword_results, confidence)
|
|
251
|
+
|
|
252
|
+
# Check if LLM was used
|
|
253
|
+
if results.get('llm_results'):
|
|
254
|
+
print("\n" + "="*70)
|
|
255
|
+
print("PHASE 2: LLM-Based Configuration Analysis")
|
|
256
|
+
print("="*70)
|
|
257
|
+
|
|
258
|
+
llm_results = results['llm_results']
|
|
259
|
+
llm_config = llm_results.get('config', {})
|
|
260
|
+
llm_confidence = llm_results.get('confidence', {})
|
|
261
|
+
llm_reasoning = llm_results.get('reasoning', {})
|
|
262
|
+
|
|
263
|
+
print("\nLLM-Detected Configuration:")
|
|
264
|
+
_display_llm_config(llm_config, llm_confidence, llm_reasoning)
|
|
265
|
+
|
|
266
|
+
# Show differences
|
|
267
|
+
_show_differences(keyword_results, llm_config)
|
|
268
|
+
|
|
269
|
+
# Final config
|
|
270
|
+
final_config = results.get('final_config')
|
|
271
|
+
if final_config:
|
|
272
|
+
print("\n" + "="*70)
|
|
273
|
+
print("FINAL CONFIGURATION (Combined)")
|
|
274
|
+
print("="*70)
|
|
275
|
+
_display_config_dict(final_config)
|
|
276
|
+
|
|
277
|
+
# User options
|
|
278
|
+
if not args.non_interactive:
|
|
279
|
+
print("\nOptions:")
|
|
280
|
+
print(" [a] Accept this configuration")
|
|
281
|
+
print(" [s] Show full details")
|
|
282
|
+
print(" [q] Quit without saving")
|
|
283
|
+
|
|
284
|
+
choice = input("\nYour choice [a]: ").lower().strip() or 'a'
|
|
285
|
+
|
|
286
|
+
if choice == 'q':
|
|
287
|
+
print("Aborted by user.")
|
|
288
|
+
sys.exit(0)
|
|
289
|
+
elif choice == 's':
|
|
290
|
+
print("\nFull Results:")
|
|
291
|
+
print(json.dumps(results, indent=2, default=str))
|
|
292
|
+
|
|
293
|
+
return results
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _display_config_summary(keyword_results: dict, confidence: dict):
|
|
297
|
+
"""Display summary of keyword-based detection."""
|
|
298
|
+
arch = keyword_results.get('architecture', 'Unknown')
|
|
299
|
+
arch_conf = confidence.get('hardware_model', 0.0)
|
|
300
|
+
time_t_size = keyword_results.get('time_t_size', 'Unknown')
|
|
301
|
+
time_t_conf = confidence.get('time_t_size_bits', 0.0)
|
|
302
|
+
|
|
303
|
+
print(f" Architecture: {arch} (confidence: {arch_conf:.2f})")
|
|
304
|
+
print(f" time_t size: {time_t_size} bits (confidence: {time_t_conf:.2f})")
|
|
305
|
+
print(f" Other fields: Low confidence (not detected by keyword-based)")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _display_llm_config(config: dict, confidence: dict, reasoning: dict):
|
|
309
|
+
"""Display LLM-detected configuration."""
|
|
310
|
+
for field, value in config.items():
|
|
311
|
+
conf = confidence.get(field, 0.0)
|
|
312
|
+
reason = reasoning.get(field, '')
|
|
313
|
+
print(f" {field}: {value} (confidence: {conf:.2f})")
|
|
314
|
+
if reason:
|
|
315
|
+
print(f" Reasoning: {reason}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _show_differences(keyword_results: dict, llm_config: dict):
|
|
319
|
+
"""Show differences between keyword-based and LLM results."""
|
|
320
|
+
differences = []
|
|
321
|
+
|
|
322
|
+
if keyword_results.get('architecture') and llm_config.get('hardware_model'):
|
|
323
|
+
if keyword_results['architecture'] != llm_config['hardware_model']:
|
|
324
|
+
differences.append((
|
|
325
|
+
'hardware_model',
|
|
326
|
+
keyword_results['architecture'],
|
|
327
|
+
llm_config['hardware_model']
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
if keyword_results.get('time_t_size') and llm_config.get('time_t_size_bits'):
|
|
331
|
+
if keyword_results['time_t_size'] != llm_config['time_t_size_bits']:
|
|
332
|
+
differences.append((
|
|
333
|
+
'time_t_size_bits',
|
|
334
|
+
keyword_results['time_t_size'],
|
|
335
|
+
llm_config['time_t_size_bits']
|
|
336
|
+
))
|
|
337
|
+
|
|
338
|
+
if differences:
|
|
339
|
+
print("\nDifferences from keyword-based detection:")
|
|
340
|
+
for field, keyword_val, llm_val in differences:
|
|
341
|
+
print(f" {field}:")
|
|
342
|
+
print(f" Keyword-based: {keyword_val}")
|
|
343
|
+
print(f" LLM: {llm_val}")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _display_config_dict(config: dict):
|
|
347
|
+
"""Display a configuration dictionary."""
|
|
348
|
+
for key, value in config.items():
|
|
349
|
+
print(f" {key}: {value}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _format_hybrid_results(results: dict, project_path: Path, format_type: str, verbose: bool) -> str:
|
|
353
|
+
"""Format hybrid detection results for output."""
|
|
354
|
+
if format_type == 'json':
|
|
355
|
+
return json.dumps(results, indent=2, default=str)
|
|
356
|
+
else:
|
|
357
|
+
# Human-readable format
|
|
358
|
+
lines = []
|
|
359
|
+
lines.append("Configuration Detection Results")
|
|
360
|
+
lines.append("=" * 50)
|
|
361
|
+
lines.append("")
|
|
362
|
+
lines.append(f"Project: {project_path}")
|
|
363
|
+
lines.append(f"Method: {results.get('method', 'unknown')}")
|
|
364
|
+
lines.append("")
|
|
365
|
+
|
|
366
|
+
# Show likelihoods from keyword results
|
|
367
|
+
keyword_results = results.get('keyword_results', {})
|
|
368
|
+
if keyword_results.get('likelihoods'):
|
|
369
|
+
lines.append("Likelihood Scores (from keyword-based detection):")
|
|
370
|
+
for likelihood in keyword_results['likelihoods']:
|
|
371
|
+
if hasattr(likelihood, 'name'):
|
|
372
|
+
# ConfigurationLikelihood object
|
|
373
|
+
symbol = "✓" if likelihood.likelihood > 0.7 else "⚠" if likelihood.likelihood > 0.3 else "○"
|
|
374
|
+
lines.append(f" {symbol} {likelihood.name:25s} {int(likelihood.likelihood * 100):>3d}%")
|
|
375
|
+
else:
|
|
376
|
+
# Dictionary
|
|
377
|
+
name = likelihood.get('name', 'unknown')
|
|
378
|
+
likelihood_val = likelihood.get('likelihood', 0.0)
|
|
379
|
+
symbol = "✓" if likelihood_val > 0.7 else "⚠" if likelihood_val > 0.3 else "○"
|
|
380
|
+
lines.append(f" {symbol} {name:25s} {int(likelihood_val * 100):>3d}%")
|
|
381
|
+
lines.append("")
|
|
382
|
+
|
|
383
|
+
# Show LLM results if available
|
|
384
|
+
llm_results = results.get('llm_results')
|
|
385
|
+
if llm_results:
|
|
386
|
+
lines.append("LLM Analysis Results:")
|
|
387
|
+
llm_config = llm_results.get('config', {})
|
|
388
|
+
for key, value in llm_config.items():
|
|
389
|
+
lines.append(f" {key}: {value}")
|
|
390
|
+
lines.append("")
|
|
391
|
+
|
|
392
|
+
# Show final config if available
|
|
393
|
+
final_config = results.get('final_config')
|
|
394
|
+
if final_config:
|
|
395
|
+
lines.append("Final Configuration (Combined):")
|
|
396
|
+
for key, value in final_config.items():
|
|
397
|
+
lines.append(f" {key}: {value}")
|
|
398
|
+
|
|
399
|
+
return "\n".join(lines)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if __name__ == '__main__':
|
|
403
|
+
main()
|