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.
Files changed (94) hide show
  1. config_detector/__init__.py +11 -0
  2. config_detector/analyzers/__init__.py +9 -0
  3. config_detector/analyzers/architecture.py +188 -0
  4. config_detector/analyzers/time_t_config.py +183 -0
  5. config_detector/build_parsers/__init__.py +10 -0
  6. config_detector/build_parsers/base.py +82 -0
  7. config_detector/build_parsers/cmake.py +113 -0
  8. config_detector/build_parsers/makefile.py +112 -0
  9. config_detector/cache.py +109 -0
  10. config_detector/cli.py +403 -0
  11. config_detector/detector.py +106 -0
  12. config_detector/hybrid_detector.py +384 -0
  13. config_detector/likelihoods.py +166 -0
  14. config_detector/llm_analyzer.py +368 -0
  15. config_detector/output/__init__.py +8 -0
  16. config_detector/output/likelihood.py +314 -0
  17. config_detector/validator.py +147 -0
  18. envui/__init__.py +4 -0
  19. envui/cli/__init__.py +4 -0
  20. envui/cli/derive_fields.py +147 -0
  21. envui/cli/env_wizard.py +381 -0
  22. envui/cli/io_utils.py +139 -0
  23. envui/cli/validate_env.py +156 -0
  24. envui/examples/env_config.sample.json +15 -0
  25. envui/schemas/environment.schema.json +151 -0
  26. report_renderer/__init__.py +8 -0
  27. report_renderer/__main__.py +8 -0
  28. report_renderer/cli.py +165 -0
  29. report_renderer/core.py +535 -0
  30. report_renderer/fixtures/malformed.json +20 -0
  31. report_renderer/fixtures/missing-fields.json +22 -0
  32. report_renderer/fixtures/typical.json +38 -0
  33. report_renderer/renderers.py +743 -0
  34. tacs/__init__.py +6 -0
  35. tacs/batch_render_reports.py +434 -0
  36. tacs/batch_repo_scan.py +390 -0
  37. tacs/batch_scan_repos.py +1556 -0
  38. tacs/cli.py +157 -0
  39. tacs/core/__init__.py +4 -0
  40. tacs/core/alias_scope.py +256 -0
  41. tacs/core/arithmetic_scanner.py +212 -0
  42. tacs/core/candidate_evidence.py +220 -0
  43. tacs/core/candidate_utils.py +238 -0
  44. tacs/core/config_validator.py +216 -0
  45. tacs/core/define_scanner.py +325 -0
  46. tacs/core/discovery_manager.py +389 -0
  47. tacs/core/env_capabilities.py +95 -0
  48. tacs/core/file_limits.py +68 -0
  49. tacs/core/function_analysis_response.py +438 -0
  50. tacs/core/function_analyzer.py +458 -0
  51. tacs/core/function_llm_client.py +1100 -0
  52. tacs/core/function_scanner.py +485 -0
  53. tacs/core/function_schemas.py +105 -0
  54. tacs/core/include_patterns.py +27 -0
  55. tacs/core/io_boundary_analyzer.py +706 -0
  56. tacs/core/io_format_parser.py +176 -0
  57. tacs/core/ir_adapter.py +465 -0
  58. tacs/core/ir_match.py +410 -0
  59. tacs/core/llm_client.py +2093 -0
  60. tacs/core/llm_errors.py +360 -0
  61. tacs/core/llm_logger.py +42 -0
  62. tacs/core/llm_prompt.py +45 -0
  63. tacs/core/llm_response.py +34 -0
  64. tacs/core/logging_config.py +128 -0
  65. tacs/core/metrics.py +92 -0
  66. tacs/core/migration_analyzer.py +373 -0
  67. tacs/core/model_assessment.py +368 -0
  68. tacs/core/path_utils.py +190 -0
  69. tacs/core/pipeline.py +3363 -0
  70. tacs/core/rule_catalog.py +206 -0
  71. tacs/core/run_ids.py +34 -0
  72. tacs/core/scan_session.py +761 -0
  73. tacs/core/schema.py +484 -0
  74. tacs/core/source_files.py +149 -0
  75. tacs/core/status_logger.py +97 -0
  76. tacs/core/struct_filter.py +108 -0
  77. tacs/core/typedef_scanner.py +246 -0
  78. tacs/fixtures/repo_lists/test_ilp32_repos.jsonl +7 -0
  79. tacs/fixtures/repo_lists/test_repos.jsonl +10 -0
  80. tacs/fixtures/repo_lists/test_time_t_size_repos.jsonl +7 -0
  81. tacs/llm/__init__.py +43 -0
  82. tacs/llm/env.py +166 -0
  83. tacs/llm/factory.py +29 -0
  84. tacs/llm/openai_models.py +187 -0
  85. tacs/python/y2038scan_fast_json_group.py +833 -0
  86. tacs/rules/retired_rule_ids.json +1 -0
  87. tacs/rules/y2038_sample_rules.json +6134 -0
  88. tacs/scan_command.py +288 -0
  89. time_assurance_code_scanner-0.2.0rc2.dist-info/METADATA +184 -0
  90. time_assurance_code_scanner-0.2.0rc2.dist-info/RECORD +94 -0
  91. time_assurance_code_scanner-0.2.0rc2.dist-info/WHEEL +5 -0
  92. time_assurance_code_scanner-0.2.0rc2.dist-info/entry_points.txt +2 -0
  93. time_assurance_code_scanner-0.2.0rc2.dist-info/licenses/LICENSE +203 -0
  94. 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
+ }
@@ -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()