serenecode 0.1.0__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.
- serenecode/__init__.py +281 -0
- serenecode/adapters/__init__.py +6 -0
- serenecode/adapters/coverage_adapter.py +1173 -0
- serenecode/adapters/crosshair_adapter.py +1069 -0
- serenecode/adapters/hypothesis_adapter.py +1824 -0
- serenecode/adapters/local_fs.py +169 -0
- serenecode/adapters/module_loader.py +492 -0
- serenecode/adapters/mypy_adapter.py +161 -0
- serenecode/checker/__init__.py +6 -0
- serenecode/checker/compositional.py +2216 -0
- serenecode/checker/coverage.py +186 -0
- serenecode/checker/properties.py +154 -0
- serenecode/checker/structural.py +1504 -0
- serenecode/checker/symbolic.py +178 -0
- serenecode/checker/types.py +148 -0
- serenecode/cli.py +478 -0
- serenecode/config.py +711 -0
- serenecode/contracts/__init__.py +6 -0
- serenecode/contracts/predicates.py +176 -0
- serenecode/core/__init__.py +6 -0
- serenecode/core/exceptions.py +38 -0
- serenecode/core/pipeline.py +807 -0
- serenecode/init.py +307 -0
- serenecode/models.py +308 -0
- serenecode/ports/__init__.py +6 -0
- serenecode/ports/coverage_analyzer.py +124 -0
- serenecode/ports/file_system.py +95 -0
- serenecode/ports/property_tester.py +69 -0
- serenecode/ports/symbolic_checker.py +70 -0
- serenecode/ports/type_checker.py +66 -0
- serenecode/reporter.py +346 -0
- serenecode/source_discovery.py +319 -0
- serenecode/templates/__init__.py +5 -0
- serenecode/templates/content.py +337 -0
- serenecode-0.1.0.dist-info/METADATA +298 -0
- serenecode-0.1.0.dist-info/RECORD +39 -0
- serenecode-0.1.0.dist-info/WHEEL +4 -0
- serenecode-0.1.0.dist-info/entry_points.txt +2 -0
- serenecode-0.1.0.dist-info/licenses/LICENSE +21 -0
serenecode/cli.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""CLI entry point for Serenecode.
|
|
2
|
+
|
|
3
|
+
This module is the composition root for the command-line interface.
|
|
4
|
+
It wires adapters to ports and delegates to core logic. As a thin
|
|
5
|
+
adapter layer, it is exempt from full contract requirements but
|
|
6
|
+
must have type annotations and pass mypy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
from serenecode.adapters.local_fs import LocalFileReader, LocalFileWriter
|
|
18
|
+
from serenecode.config import parse_serenecode_md
|
|
19
|
+
from serenecode.contracts.predicates import (
|
|
20
|
+
is_non_empty_string,
|
|
21
|
+
is_positive_int,
|
|
22
|
+
is_valid_exit_code,
|
|
23
|
+
is_valid_template_name,
|
|
24
|
+
is_valid_verification_level,
|
|
25
|
+
)
|
|
26
|
+
from serenecode.core.pipeline import run_pipeline
|
|
27
|
+
from serenecode.init import initialize_project
|
|
28
|
+
from serenecode.models import CheckResult, ExitCode
|
|
29
|
+
from serenecode.reporter import format_html, format_human, format_json
|
|
30
|
+
from serenecode.source_discovery import build_source_files, find_serenecode_md
|
|
31
|
+
|
|
32
|
+
_TRUST_REQUIRED_MESSAGE = (
|
|
33
|
+
"Levels 3-6 import and execute project modules. "
|
|
34
|
+
"Re-run with --allow-code-execution only for trusted code."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group()
|
|
39
|
+
@icontract.ensure(lambda result: result is None, "CLI entrypoint returns None")
|
|
40
|
+
def main() -> None:
|
|
41
|
+
"""Serenecode — formal verification for AI-generated Python code."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@main.command()
|
|
45
|
+
@click.option("--strict", "template", flag_value="strict", help="Use strict template (all rules mandatory)")
|
|
46
|
+
@click.option("--minimal", "template", flag_value="minimal", help="Use minimal template (contracts + types only)")
|
|
47
|
+
@click.argument("path", default=".")
|
|
48
|
+
@icontract.require(
|
|
49
|
+
lambda template: template is None or is_valid_template_name(template),
|
|
50
|
+
"template must be a recognized template name when provided",
|
|
51
|
+
)
|
|
52
|
+
@icontract.require(lambda path: is_non_empty_string(path), "path must be a non-empty string")
|
|
53
|
+
@icontract.ensure(lambda result: result is None, "CLI commands return None")
|
|
54
|
+
def init(template: str | None, path: str) -> None:
|
|
55
|
+
"""Initialize a Serenecode project."""
|
|
56
|
+
if template is None:
|
|
57
|
+
template = "default"
|
|
58
|
+
reader = LocalFileReader()
|
|
59
|
+
writer = LocalFileWriter()
|
|
60
|
+
|
|
61
|
+
def confirm(message: str) -> bool:
|
|
62
|
+
return click.confirm(message, default=True)
|
|
63
|
+
|
|
64
|
+
result = initialize_project(
|
|
65
|
+
directory=path,
|
|
66
|
+
template=template,
|
|
67
|
+
file_reader=reader,
|
|
68
|
+
file_writer=writer,
|
|
69
|
+
confirm_callback=confirm,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if result.serenecode_md_created:
|
|
73
|
+
click.echo(f"Created SERENECODE.md ({template} template)")
|
|
74
|
+
if result.claude_md_created:
|
|
75
|
+
click.echo("Created CLAUDE.md with Serenecode directive")
|
|
76
|
+
if result.claude_md_updated:
|
|
77
|
+
click.echo("Updated CLAUDE.md with Serenecode directive")
|
|
78
|
+
|
|
79
|
+
click.echo("Serenecode project initialized.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@main.command()
|
|
83
|
+
@click.argument("path", default=".")
|
|
84
|
+
@click.option("--level", type=click.IntRange(1, 6), default=None, help="Verification level (1-6, default: from config template)")
|
|
85
|
+
@click.option(
|
|
86
|
+
"--format",
|
|
87
|
+
"output_format",
|
|
88
|
+
type=click.Choice(["human", "json"]),
|
|
89
|
+
default="human",
|
|
90
|
+
help="Output format",
|
|
91
|
+
)
|
|
92
|
+
@click.option("--structural", is_flag=True, help="Run only structural check (Level 1)")
|
|
93
|
+
@click.option("--verify", is_flag=True, help="Run Levels 3-6 only")
|
|
94
|
+
@click.option("--per-condition-timeout", type=int, default=30, show_default=True, help="Timeout in seconds per condition for symbolic verification (Level 5)")
|
|
95
|
+
@click.option("--per-path-timeout", type=int, default=10, show_default=True, help="Timeout in seconds per execution path for symbolic verification (Level 5)")
|
|
96
|
+
@click.option("--module-timeout", type=int, default=300, show_default=True, help="Timeout in seconds per module for symbolic verification (Level 5)")
|
|
97
|
+
@click.option("--workers", type=int, default=4, show_default=True, help="Number of parallel workers for symbolic verification (Level 5)")
|
|
98
|
+
@click.option(
|
|
99
|
+
"--allow-code-execution",
|
|
100
|
+
is_flag=True,
|
|
101
|
+
help="Allow Levels 3-6 to import and execute project modules",
|
|
102
|
+
)
|
|
103
|
+
@icontract.require(lambda path: is_non_empty_string(path), "path must be a non-empty string")
|
|
104
|
+
@icontract.require(
|
|
105
|
+
lambda level: level is None or is_valid_verification_level(level),
|
|
106
|
+
"level must be between 1 and 6 when provided",
|
|
107
|
+
)
|
|
108
|
+
@icontract.require(
|
|
109
|
+
lambda output_format: output_format in {"human", "json"},
|
|
110
|
+
"output_format must be human or json",
|
|
111
|
+
)
|
|
112
|
+
@icontract.require(
|
|
113
|
+
lambda per_condition_timeout: is_positive_int(per_condition_timeout),
|
|
114
|
+
"per_condition_timeout must be at least 1",
|
|
115
|
+
)
|
|
116
|
+
@icontract.require(
|
|
117
|
+
lambda per_path_timeout: is_positive_int(per_path_timeout),
|
|
118
|
+
"per_path_timeout must be at least 1",
|
|
119
|
+
)
|
|
120
|
+
@icontract.require(
|
|
121
|
+
lambda module_timeout: is_positive_int(module_timeout),
|
|
122
|
+
"module_timeout must be at least 1",
|
|
123
|
+
)
|
|
124
|
+
@icontract.require(
|
|
125
|
+
lambda workers: is_positive_int(workers),
|
|
126
|
+
"workers must be at least 1",
|
|
127
|
+
)
|
|
128
|
+
@icontract.ensure(lambda result: result is None, "CLI commands return None")
|
|
129
|
+
def check(
|
|
130
|
+
path: str,
|
|
131
|
+
level: int | None,
|
|
132
|
+
output_format: str,
|
|
133
|
+
structural: bool,
|
|
134
|
+
verify: bool,
|
|
135
|
+
per_condition_timeout: int,
|
|
136
|
+
per_path_timeout: int,
|
|
137
|
+
module_timeout: int,
|
|
138
|
+
workers: int,
|
|
139
|
+
allow_code_execution: bool,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Run verification checks on Python source files."""
|
|
142
|
+
wall_start = time.monotonic()
|
|
143
|
+
reader = LocalFileReader()
|
|
144
|
+
|
|
145
|
+
# Load config first (needed to resolve default level)
|
|
146
|
+
serenecode_md_path = find_serenecode_md(path, reader)
|
|
147
|
+
if serenecode_md_path:
|
|
148
|
+
config_content = reader.read_file(serenecode_md_path)
|
|
149
|
+
config = parse_serenecode_md(config_content)
|
|
150
|
+
else:
|
|
151
|
+
from serenecode.config import default_config
|
|
152
|
+
config = default_config()
|
|
153
|
+
click.echo("Warning: No SERENECODE.md found, using default configuration.", err=True)
|
|
154
|
+
|
|
155
|
+
# Determine effective level
|
|
156
|
+
if structural:
|
|
157
|
+
effective_level = 1
|
|
158
|
+
elif level is not None:
|
|
159
|
+
effective_level = level
|
|
160
|
+
if verify:
|
|
161
|
+
effective_level = max(effective_level, 3)
|
|
162
|
+
else:
|
|
163
|
+
effective_level = config.recommended_level
|
|
164
|
+
if verify:
|
|
165
|
+
effective_level = max(effective_level, 3)
|
|
166
|
+
level = effective_level
|
|
167
|
+
start_level = 3 if verify and not structural else 1
|
|
168
|
+
|
|
169
|
+
if level >= 3 and not allow_code_execution:
|
|
170
|
+
click.echo(f"Error: {_TRUST_REQUIRED_MESSAGE}", err=True)
|
|
171
|
+
sys.exit(ExitCode.INTERNAL)
|
|
172
|
+
|
|
173
|
+
# List files
|
|
174
|
+
try:
|
|
175
|
+
files = reader.list_python_files(path)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
click.echo(f"Error: {exc}", err=True)
|
|
178
|
+
sys.exit(ExitCode.INTERNAL)
|
|
179
|
+
|
|
180
|
+
if not files:
|
|
181
|
+
click.echo("No Python files found.")
|
|
182
|
+
sys.exit(ExitCode.PASSED)
|
|
183
|
+
|
|
184
|
+
# Build source file objects
|
|
185
|
+
try:
|
|
186
|
+
source_files = build_source_files(files, reader, path)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
click.echo(f"Error: {exc}", err=True)
|
|
189
|
+
sys.exit(ExitCode.INTERNAL)
|
|
190
|
+
|
|
191
|
+
# Wire up adapters for higher levels
|
|
192
|
+
type_checker = None
|
|
193
|
+
coverage_analyzer = None
|
|
194
|
+
property_tester = None
|
|
195
|
+
symbolic_checker = None
|
|
196
|
+
|
|
197
|
+
if level >= 2:
|
|
198
|
+
try:
|
|
199
|
+
from serenecode.adapters.mypy_adapter import MypyTypeChecker
|
|
200
|
+
type_checker = MypyTypeChecker()
|
|
201
|
+
except ImportError:
|
|
202
|
+
click.echo("Warning: mypy not available for Level 2 checks.", err=True)
|
|
203
|
+
|
|
204
|
+
if level >= 3:
|
|
205
|
+
try:
|
|
206
|
+
from serenecode.adapters.coverage_adapter import CoverageAnalyzerAdapter
|
|
207
|
+
coverage_analyzer = CoverageAnalyzerAdapter(allow_code_execution=True)
|
|
208
|
+
except ImportError:
|
|
209
|
+
click.echo("Warning: coverage not available for Level 3 checks.", err=True)
|
|
210
|
+
|
|
211
|
+
if level >= 4:
|
|
212
|
+
try:
|
|
213
|
+
from serenecode.adapters.hypothesis_adapter import HypothesisPropertyTester
|
|
214
|
+
property_tester = HypothesisPropertyTester(allow_code_execution=True)
|
|
215
|
+
except ImportError:
|
|
216
|
+
click.echo("Warning: Hypothesis not available for Level 4 checks.", err=True)
|
|
217
|
+
|
|
218
|
+
if level >= 5:
|
|
219
|
+
try:
|
|
220
|
+
from serenecode.adapters.crosshair_adapter import CrossHairSymbolicChecker
|
|
221
|
+
symbolic_checker = CrossHairSymbolicChecker(
|
|
222
|
+
per_condition_timeout=per_condition_timeout,
|
|
223
|
+
per_path_timeout=per_path_timeout,
|
|
224
|
+
module_timeout=module_timeout,
|
|
225
|
+
allow_code_execution=True,
|
|
226
|
+
)
|
|
227
|
+
except ImportError:
|
|
228
|
+
click.echo("Warning: CrossHair not available for Level 5 checks.", err=True)
|
|
229
|
+
|
|
230
|
+
# Run pipeline with progress callback
|
|
231
|
+
def _progress(msg: str) -> None:
|
|
232
|
+
click.echo(msg, err=True)
|
|
233
|
+
|
|
234
|
+
final_result = run_pipeline(
|
|
235
|
+
source_files=source_files,
|
|
236
|
+
level=level,
|
|
237
|
+
start_level=start_level,
|
|
238
|
+
config=config,
|
|
239
|
+
type_checker=type_checker,
|
|
240
|
+
coverage_analyzer=coverage_analyzer,
|
|
241
|
+
property_tester=property_tester,
|
|
242
|
+
symbolic_checker=symbolic_checker,
|
|
243
|
+
progress=_progress,
|
|
244
|
+
max_workers=workers,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Format and output
|
|
248
|
+
if output_format == "json":
|
|
249
|
+
click.echo(format_json(final_result))
|
|
250
|
+
else:
|
|
251
|
+
click.echo(format_human(final_result))
|
|
252
|
+
|
|
253
|
+
wall_elapsed = time.monotonic() - wall_start
|
|
254
|
+
minutes, seconds = divmod(wall_elapsed, 60)
|
|
255
|
+
if minutes >= 1:
|
|
256
|
+
click.echo(f"Total wall time: {int(minutes)}m {seconds:.1f}s", err=True)
|
|
257
|
+
else:
|
|
258
|
+
click.echo(f"Total wall time: {seconds:.1f}s", err=True)
|
|
259
|
+
|
|
260
|
+
# Exit with appropriate code
|
|
261
|
+
if final_result.passed:
|
|
262
|
+
sys.exit(ExitCode.PASSED)
|
|
263
|
+
else:
|
|
264
|
+
# Find the lowest failing level from the results
|
|
265
|
+
exit_code = _determine_exit_code(final_result)
|
|
266
|
+
sys.exit(exit_code)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@main.command()
|
|
270
|
+
@click.argument("path", default=".")
|
|
271
|
+
@click.option(
|
|
272
|
+
"--format",
|
|
273
|
+
"output_format",
|
|
274
|
+
type=click.Choice(["human", "json"]),
|
|
275
|
+
default="human",
|
|
276
|
+
help="Output format",
|
|
277
|
+
)
|
|
278
|
+
@icontract.require(lambda path: is_non_empty_string(path), "path must be a non-empty string")
|
|
279
|
+
@icontract.require(
|
|
280
|
+
lambda output_format: output_format in {"human", "json"},
|
|
281
|
+
"output_format must be human or json",
|
|
282
|
+
)
|
|
283
|
+
@icontract.ensure(lambda result: result is None, "CLI commands return None")
|
|
284
|
+
def status(path: str, output_format: str) -> None:
|
|
285
|
+
"""Show verification status of the codebase."""
|
|
286
|
+
reader = LocalFileReader()
|
|
287
|
+
|
|
288
|
+
# Load config
|
|
289
|
+
serenecode_md_path = find_serenecode_md(path, reader)
|
|
290
|
+
if serenecode_md_path:
|
|
291
|
+
config_content = reader.read_file(serenecode_md_path)
|
|
292
|
+
config = parse_serenecode_md(config_content)
|
|
293
|
+
else:
|
|
294
|
+
from serenecode.config import default_config
|
|
295
|
+
config = default_config()
|
|
296
|
+
|
|
297
|
+
# List and check files
|
|
298
|
+
try:
|
|
299
|
+
files = reader.list_python_files(path)
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
click.echo(f"Error: {exc}", err=True)
|
|
302
|
+
sys.exit(ExitCode.INTERNAL)
|
|
303
|
+
|
|
304
|
+
if not files:
|
|
305
|
+
click.echo("No Python files found.")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
source_files = build_source_files(files, reader, path)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
click.echo(f"Error: {exc}", err=True)
|
|
312
|
+
sys.exit(ExitCode.INTERNAL)
|
|
313
|
+
result = run_pipeline(source_files, level=1, start_level=1, config=config)
|
|
314
|
+
|
|
315
|
+
if output_format == "json":
|
|
316
|
+
click.echo(format_json(result))
|
|
317
|
+
else:
|
|
318
|
+
click.echo(format_human(result))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@main.command()
|
|
322
|
+
@click.argument("path", default=".")
|
|
323
|
+
@click.option(
|
|
324
|
+
"--format",
|
|
325
|
+
"output_format",
|
|
326
|
+
type=click.Choice(["human", "json", "html"]),
|
|
327
|
+
default="human",
|
|
328
|
+
help="Report format",
|
|
329
|
+
)
|
|
330
|
+
@click.option("--output", "output_file", default=None, help="Write report to file")
|
|
331
|
+
@click.option(
|
|
332
|
+
"--allow-code-execution",
|
|
333
|
+
is_flag=True,
|
|
334
|
+
help="Allow deep reports to import and execute project modules",
|
|
335
|
+
)
|
|
336
|
+
@icontract.require(lambda path: is_non_empty_string(path), "path must be a non-empty string")
|
|
337
|
+
@icontract.require(
|
|
338
|
+
lambda output_format: output_format in {"human", "json", "html"},
|
|
339
|
+
"output_format must be human, json, or html",
|
|
340
|
+
)
|
|
341
|
+
@icontract.require(
|
|
342
|
+
lambda output_file: output_file is None or is_non_empty_string(output_file),
|
|
343
|
+
"output_file must be a non-empty string when provided",
|
|
344
|
+
)
|
|
345
|
+
@icontract.ensure(lambda result: result is None, "CLI commands return None")
|
|
346
|
+
def report(
|
|
347
|
+
path: str,
|
|
348
|
+
output_format: str,
|
|
349
|
+
output_file: str | None,
|
|
350
|
+
allow_code_execution: bool,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Generate a verification report for the entire project."""
|
|
353
|
+
reader = LocalFileReader()
|
|
354
|
+
|
|
355
|
+
# Load config
|
|
356
|
+
serenecode_md_path = find_serenecode_md(path, reader)
|
|
357
|
+
if serenecode_md_path:
|
|
358
|
+
config_content = reader.read_file(serenecode_md_path)
|
|
359
|
+
config = parse_serenecode_md(config_content)
|
|
360
|
+
else:
|
|
361
|
+
from serenecode.config import default_config
|
|
362
|
+
config = default_config()
|
|
363
|
+
|
|
364
|
+
# List and check files
|
|
365
|
+
try:
|
|
366
|
+
files = reader.list_python_files(path)
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
click.echo(f"Error: {exc}", err=True)
|
|
369
|
+
sys.exit(ExitCode.INTERNAL)
|
|
370
|
+
|
|
371
|
+
if not files:
|
|
372
|
+
click.echo("No Python files found.")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
source_files = build_source_files(files, reader, path)
|
|
377
|
+
except Exception as exc:
|
|
378
|
+
click.echo(f"Error: {exc}", err=True)
|
|
379
|
+
sys.exit(ExitCode.INTERNAL)
|
|
380
|
+
# Reports use the project's recommended verification depth rather than
|
|
381
|
+
# silently truncating to structural checks only.
|
|
382
|
+
level = config.recommended_level
|
|
383
|
+
if level >= 3 and not allow_code_execution:
|
|
384
|
+
click.echo(f"Error: {_TRUST_REQUIRED_MESSAGE}", err=True)
|
|
385
|
+
sys.exit(ExitCode.INTERNAL)
|
|
386
|
+
type_checker = None
|
|
387
|
+
coverage_analyzer = None
|
|
388
|
+
property_tester = None
|
|
389
|
+
symbolic_checker = None
|
|
390
|
+
|
|
391
|
+
if level >= 2:
|
|
392
|
+
try:
|
|
393
|
+
from serenecode.adapters.mypy_adapter import MypyTypeChecker
|
|
394
|
+
type_checker = MypyTypeChecker()
|
|
395
|
+
except ImportError:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
if level >= 3:
|
|
399
|
+
try:
|
|
400
|
+
from serenecode.adapters.coverage_adapter import CoverageAnalyzerAdapter
|
|
401
|
+
coverage_analyzer = CoverageAnalyzerAdapter(allow_code_execution=True)
|
|
402
|
+
except ImportError:
|
|
403
|
+
click.echo("Warning: coverage not available for Level 3 checks.", err=True)
|
|
404
|
+
|
|
405
|
+
if level >= 4:
|
|
406
|
+
try:
|
|
407
|
+
from serenecode.adapters.hypothesis_adapter import HypothesisPropertyTester
|
|
408
|
+
property_tester = HypothesisPropertyTester(allow_code_execution=True)
|
|
409
|
+
except ImportError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
if level >= 5:
|
|
413
|
+
try:
|
|
414
|
+
from serenecode.adapters.crosshair_adapter import CrossHairSymbolicChecker
|
|
415
|
+
symbolic_checker = CrossHairSymbolicChecker(allow_code_execution=True)
|
|
416
|
+
except ImportError:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
final_result = run_pipeline(
|
|
420
|
+
source_files,
|
|
421
|
+
level=level,
|
|
422
|
+
start_level=1,
|
|
423
|
+
config=config,
|
|
424
|
+
type_checker=type_checker,
|
|
425
|
+
coverage_analyzer=coverage_analyzer,
|
|
426
|
+
property_tester=property_tester,
|
|
427
|
+
symbolic_checker=symbolic_checker,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Format output
|
|
431
|
+
if output_format == "json":
|
|
432
|
+
formatted = format_json(final_result)
|
|
433
|
+
elif output_format == "html":
|
|
434
|
+
formatted = format_html(final_result)
|
|
435
|
+
else:
|
|
436
|
+
formatted = format_human(final_result)
|
|
437
|
+
|
|
438
|
+
# Write to file or stdout
|
|
439
|
+
if output_file:
|
|
440
|
+
writer = LocalFileWriter()
|
|
441
|
+
writer.write_file(output_file, formatted)
|
|
442
|
+
click.echo(f"Report written to {output_file}")
|
|
443
|
+
else:
|
|
444
|
+
click.echo(formatted)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@icontract.require(lambda check_result: check_result is not None, "result must be provided")
|
|
448
|
+
@icontract.ensure(lambda result: is_valid_exit_code(result), "exit code must be valid")
|
|
449
|
+
def _determine_exit_code(check_result: CheckResult) -> int:
|
|
450
|
+
"""Determine the CLI exit code from a failed CheckResult.
|
|
451
|
+
|
|
452
|
+
Uses the verification level of the first failure to determine
|
|
453
|
+
the appropriate exit code per spec Section 4.2.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
check_result: A CheckResult with failures.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
An exit code integer (1-6 or 10).
|
|
460
|
+
"""
|
|
461
|
+
from serenecode.models import CheckStatus
|
|
462
|
+
|
|
463
|
+
# Find the lowest failing level across all failed results
|
|
464
|
+
min_level = 10 # start above any valid level
|
|
465
|
+
# Loop invariant: min_level is the lowest failure level seen in results[0..i]
|
|
466
|
+
for func_result in check_result.results:
|
|
467
|
+
if func_result.status == CheckStatus.FAILED:
|
|
468
|
+
# Loop invariant: checked details[0..j] for level
|
|
469
|
+
for detail in func_result.details:
|
|
470
|
+
level_val = detail.level.value
|
|
471
|
+
if 1 <= level_val <= 6 and level_val < min_level:
|
|
472
|
+
min_level = level_val
|
|
473
|
+
|
|
474
|
+
if min_level <= 6:
|
|
475
|
+
return min_level
|
|
476
|
+
if check_result.level_achieved < check_result.level_requested:
|
|
477
|
+
return min(check_result.level_achieved + 1, ExitCode.COMPOSITIONAL)
|
|
478
|
+
return ExitCode.STRUCTURAL # default to structural
|