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.
Files changed (39) hide show
  1. serenecode/__init__.py +281 -0
  2. serenecode/adapters/__init__.py +6 -0
  3. serenecode/adapters/coverage_adapter.py +1173 -0
  4. serenecode/adapters/crosshair_adapter.py +1069 -0
  5. serenecode/adapters/hypothesis_adapter.py +1824 -0
  6. serenecode/adapters/local_fs.py +169 -0
  7. serenecode/adapters/module_loader.py +492 -0
  8. serenecode/adapters/mypy_adapter.py +161 -0
  9. serenecode/checker/__init__.py +6 -0
  10. serenecode/checker/compositional.py +2216 -0
  11. serenecode/checker/coverage.py +186 -0
  12. serenecode/checker/properties.py +154 -0
  13. serenecode/checker/structural.py +1504 -0
  14. serenecode/checker/symbolic.py +178 -0
  15. serenecode/checker/types.py +148 -0
  16. serenecode/cli.py +478 -0
  17. serenecode/config.py +711 -0
  18. serenecode/contracts/__init__.py +6 -0
  19. serenecode/contracts/predicates.py +176 -0
  20. serenecode/core/__init__.py +6 -0
  21. serenecode/core/exceptions.py +38 -0
  22. serenecode/core/pipeline.py +807 -0
  23. serenecode/init.py +307 -0
  24. serenecode/models.py +308 -0
  25. serenecode/ports/__init__.py +6 -0
  26. serenecode/ports/coverage_analyzer.py +124 -0
  27. serenecode/ports/file_system.py +95 -0
  28. serenecode/ports/property_tester.py +69 -0
  29. serenecode/ports/symbolic_checker.py +70 -0
  30. serenecode/ports/type_checker.py +66 -0
  31. serenecode/reporter.py +346 -0
  32. serenecode/source_discovery.py +319 -0
  33. serenecode/templates/__init__.py +5 -0
  34. serenecode/templates/content.py +337 -0
  35. serenecode-0.1.0.dist-info/METADATA +298 -0
  36. serenecode-0.1.0.dist-info/RECORD +39 -0
  37. serenecode-0.1.0.dist-info/WHEEL +4 -0
  38. serenecode-0.1.0.dist-info/entry_points.txt +2 -0
  39. serenecode-0.1.0.dist-info/licenses/LICENSE +21 -0
serenecode/init.py ADDED
@@ -0,0 +1,307 @@
1
+ """Project initialization logic for Serenecode.
2
+
3
+ This module handles the `serenecode init` command logic. It generates
4
+ SERENECODE.md and CLAUDE.md files from templates and manages the
5
+ initialization workflow.
6
+
7
+ Core logic functions are pure (no I/O). The initialize_project
8
+ orchestrator uses ports for file system access.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+
16
+ import icontract
17
+
18
+ from serenecode.contracts.predicates import is_valid_template_name
19
+ from serenecode.ports.file_system import FileReader, FileWriter
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # SERENECODE.md template content (embedded as string constants)
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _CLAUDE_MD_SECTIONS = {
26
+ "default": """\
27
+ ## Serenecode
28
+
29
+ All code in this project MUST follow the standards defined in SERENECODE.md. \
30
+ Read SERENECODE.md before writing or modifying any code. Every public function \
31
+ with caller-supplied inputs must have icontract preconditions, and every \
32
+ public function must have postconditions. Every class must have invariants. \
33
+ Follow the architectural patterns specified in SERENECODE.md.
34
+
35
+ ### Verification
36
+
37
+ After each work iteration (implementing a feature, fixing a bug, refactoring), \
38
+ offer to run verification before considering the task complete.
39
+
40
+ **Quick structural check (seconds):**
41
+ ```bash
42
+ serenecode check src/ --structural
43
+ ```
44
+
45
+ **Full verification with property testing (minutes):**
46
+ ```bash
47
+ serenecode check src/ --level 4 --allow-code-execution
48
+ ```
49
+
50
+ **Generate an HTML report:**
51
+ ```bash
52
+ serenecode report src/ --format html --output report.html --allow-code-execution
53
+ ```
54
+
55
+ Levels 3-6 import and execute project modules. Only use \
56
+ `--allow-code-execution` for trusted code.
57
+
58
+ If verification fails, read the error messages and fix the issues. Each failure \
59
+ includes the function name, file, line number, and a suggested fix. Iterate \
60
+ until all checks pass.
61
+ """,
62
+ "strict": """\
63
+ ## Serenecode (Strict Mode)
64
+
65
+ All code in this project MUST follow the standards defined in SERENECODE.md. \
66
+ Read SERENECODE.md before writing or modifying any code. Every function — \
67
+ public and private — with caller-supplied inputs must have icontract \
68
+ preconditions, and every function must have postconditions. Every class must \
69
+ have invariants. No exemptions.
70
+
71
+ ### Verification
72
+
73
+ After each work iteration (implementing a feature, fixing a bug, refactoring), \
74
+ you MUST run verification before considering the task complete. Do not skip this.
75
+
76
+ **Quick structural check (seconds):**
77
+ ```bash
78
+ serenecode check src/ --structural
79
+ ```
80
+
81
+ **Full verification with property testing (minutes):**
82
+ ```bash
83
+ serenecode check src/ --level 4 --allow-code-execution
84
+ ```
85
+
86
+ **Full verification including symbolic and compositional (minutes):**
87
+ ```bash
88
+ serenecode check src/ --level 6 --allow-code-execution
89
+ ```
90
+
91
+ **Generate an HTML report:**
92
+ ```bash
93
+ serenecode report src/ --format html --output report.html --allow-code-execution
94
+ ```
95
+
96
+ Levels 3-6 import and execute project modules. Only use \
97
+ `--allow-code-execution` for trusted code.
98
+
99
+ If verification fails, read the error messages and fix the issues. Each failure \
100
+ includes the function name, file, line number, and a suggested fix. Iterate \
101
+ until all checks pass. Do not commit code that fails verification.
102
+ """,
103
+ "minimal": """\
104
+ ## Serenecode
105
+
106
+ All code in this project should follow the conventions in SERENECODE.md. \
107
+ Public functions with caller-supplied inputs must have icontract \
108
+ preconditions, and public functions should have postconditions.
109
+
110
+ ### Verification
111
+
112
+ After completing a feature or fix, consider running a quick check:
113
+
114
+ ```bash
115
+ serenecode check src/ --structural
116
+ ```
117
+
118
+ If issues are found, fix them before moving on.
119
+ """,
120
+ }
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Result dataclass
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ @icontract.invariant(
129
+ lambda self: not (self.claude_md_created and self.claude_md_updated),
130
+ "CLAUDE.md cannot be both created and updated in the same operation",
131
+ )
132
+ @dataclass(frozen=True)
133
+ class InitResult:
134
+ """Result of project initialization.
135
+
136
+ Indicates which files were created or updated during init.
137
+ """
138
+
139
+ serenecode_md_created: bool
140
+ claude_md_created: bool
141
+ claude_md_updated: bool
142
+ template_used: str
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Pure functions
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ @icontract.require(
151
+ lambda template: is_valid_template_name(template),
152
+ "template must be a valid template name",
153
+ )
154
+ @icontract.ensure(
155
+ lambda result: isinstance(result, str) and len(result) > 0,
156
+ "result must be a non-empty string",
157
+ )
158
+ def generate_serenecode_md(template: str) -> str:
159
+ """Return the SERENECODE.md content for the given template name.
160
+
161
+ Args:
162
+ template: One of 'default', 'strict', or 'minimal'.
163
+
164
+ Returns:
165
+ The SERENECODE.md markdown content.
166
+ """
167
+ from serenecode.templates import content as template_content
168
+
169
+ return template_content.get_template(template)
170
+
171
+
172
+ @icontract.require(
173
+ lambda template: is_valid_template_name(template),
174
+ "template must be a valid template name",
175
+ )
176
+ @icontract.ensure(
177
+ lambda result: isinstance(result, str) and len(result) > 0,
178
+ "result must be a non-empty string",
179
+ )
180
+ def generate_claude_md_section(template: str = "default") -> str:
181
+ """Return the Serenecode directive section for CLAUDE.md.
182
+
183
+ The section content varies by template — strict mode uses stronger
184
+ language and requires verification before commits.
185
+
186
+ Args:
187
+ template: The template name ('default', 'strict', or 'minimal').
188
+
189
+ Returns:
190
+ The markdown section to add to CLAUDE.md.
191
+ """
192
+ return _CLAUDE_MD_SECTIONS[template]
193
+
194
+
195
+ @icontract.require(
196
+ lambda serenecode_section: isinstance(serenecode_section, str) and len(serenecode_section) > 0,
197
+ "serenecode_section must be a non-empty string",
198
+ )
199
+ @icontract.ensure(
200
+ lambda result: isinstance(result, str),
201
+ "result must be a string",
202
+ )
203
+ def merge_claude_md(existing_content: str | None, serenecode_section: str) -> str:
204
+ """Merge the Serenecode section into existing CLAUDE.md content.
205
+
206
+ If the existing content already contains "## Serenecode", do not
207
+ add a duplicate. Otherwise, append the section.
208
+
209
+ Args:
210
+ existing_content: Current CLAUDE.md content, or None if no file exists.
211
+ serenecode_section: The Serenecode directive section to add.
212
+
213
+ Returns:
214
+ The merged CLAUDE.md content.
215
+ """
216
+ if existing_content is None:
217
+ return serenecode_section
218
+
219
+ if "## Serenecode" in existing_content:
220
+ return existing_content
221
+
222
+ return existing_content.rstrip() + "\n\n" + serenecode_section
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Orchestrator (uses ports)
227
+ # ---------------------------------------------------------------------------
228
+
229
+
230
+ @icontract.require(
231
+ lambda template: is_valid_template_name(template),
232
+ "template must be a valid template name",
233
+ )
234
+ @icontract.ensure(
235
+ lambda result: isinstance(result, InitResult),
236
+ "result must be an InitResult",
237
+ )
238
+ def initialize_project(
239
+ directory: str,
240
+ template: str,
241
+ file_reader: FileReader,
242
+ file_writer: FileWriter,
243
+ confirm_callback: Callable[[str], bool] | None = None,
244
+ ) -> InitResult:
245
+ """Initialize a Serenecode project in the given directory.
246
+
247
+ Creates SERENECODE.md from the selected template and sets up
248
+ CLAUDE.md with the Serenecode directive.
249
+
250
+ Args:
251
+ directory: Project root directory path.
252
+ template: Template name ('default', 'strict', or 'minimal').
253
+ file_reader: A FileReader implementation.
254
+ file_writer: A FileWriter implementation.
255
+ confirm_callback: Optional callback for user confirmation prompts.
256
+ If None, proceeds without confirmation.
257
+
258
+ Returns:
259
+ An InitResult describing what was created/modified.
260
+ """
261
+ import os
262
+
263
+ serenecode_path = os.path.join(directory, "SERENECODE.md")
264
+ claude_path = os.path.join(directory, "CLAUDE.md")
265
+
266
+ serenecode_md_created = False
267
+ claude_md_created = False
268
+ claude_md_updated = False
269
+
270
+ # Generate and write SERENECODE.md
271
+ serenecode_exists = file_reader.file_exists(serenecode_path)
272
+ should_write_serenecode = True
273
+ if serenecode_exists and confirm_callback is not None:
274
+ should_write_serenecode = confirm_callback(
275
+ "SERENECODE.md already exists. Overwrite?"
276
+ )
277
+
278
+ if should_write_serenecode:
279
+ content = generate_serenecode_md(template)
280
+ file_writer.write_file(serenecode_path, content)
281
+ serenecode_md_created = True
282
+
283
+ # Generate and write/update CLAUDE.md
284
+ claude_section = generate_claude_md_section(template)
285
+ claude_exists = file_reader.file_exists(claude_path)
286
+ if claude_exists:
287
+ existing = file_reader.read_file(claude_path)
288
+ if "## Serenecode" not in existing:
289
+ should_update = True
290
+ if confirm_callback is not None:
291
+ should_update = confirm_callback(
292
+ "Add Serenecode directive to existing CLAUDE.md?"
293
+ )
294
+ if should_update:
295
+ merged = merge_claude_md(existing, claude_section)
296
+ file_writer.write_file(claude_path, merged)
297
+ claude_md_updated = True
298
+ else:
299
+ file_writer.write_file(claude_path, claude_section)
300
+ claude_md_created = True
301
+
302
+ return InitResult(
303
+ serenecode_md_created=serenecode_md_created,
304
+ claude_md_created=claude_md_created,
305
+ claude_md_updated=claude_md_updated,
306
+ template_used=template,
307
+ )
serenecode/models.py ADDED
@@ -0,0 +1,308 @@
1
+ """Data models for Serenecode verification results.
2
+
3
+ This module defines the core data structures used throughout Serenecode
4
+ to represent verification results. All models are frozen dataclasses
5
+ with icontract invariants to enforce correctness.
6
+
7
+ This is a core module — no I/O imports are permitted.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum, IntEnum
15
+
16
+ import icontract
17
+
18
+ from serenecode.contracts.predicates import is_non_empty_string, is_non_negative_int
19
+
20
+
21
+ class VerificationLevel(Enum):
22
+ """Verification levels in the Serenecode pipeline."""
23
+
24
+ STRUCTURAL = 1
25
+ TYPES = 2
26
+ COVERAGE = 3
27
+ PROPERTIES = 4
28
+ SYMBOLIC = 5
29
+ COMPOSITIONAL = 6
30
+
31
+
32
+ class CheckStatus(Enum):
33
+ """Status of a verification check."""
34
+
35
+ PASSED = "passed"
36
+ FAILED = "failed"
37
+ SKIPPED = "skipped"
38
+ EXEMPT = "exempt"
39
+
40
+
41
+ class ExitCode(IntEnum):
42
+ """Exit codes for the Serenecode CLI."""
43
+
44
+ PASSED = 0
45
+ STRUCTURAL = 1
46
+ TYPES = 2
47
+ COVERAGE = 3
48
+ PROPERTIES = 4
49
+ SYMBOLIC = 5
50
+ COMPOSITIONAL = 6
51
+ INTERNAL = 10
52
+
53
+
54
+ @icontract.invariant(
55
+ lambda self: is_non_empty_string(self.message),
56
+ "message must be a non-empty string",
57
+ )
58
+ @dataclass(frozen=True)
59
+ class Detail:
60
+ """A single verification finding.
61
+
62
+ Represents one specific issue or confirmation found during
63
+ verification at a particular level.
64
+ """
65
+
66
+ level: VerificationLevel
67
+ tool: str
68
+ finding_type: str
69
+ message: str
70
+ counterexample: dict[str, object] | None = None
71
+ suggestion: str | None = None
72
+
73
+ @icontract.ensure(
74
+ lambda result: isinstance(result, dict),
75
+ "result must be a dictionary",
76
+ )
77
+ def to_dict(self) -> dict[str, object]:
78
+ """Convert to a plain dictionary for serialization."""
79
+ result: dict[str, object] = {
80
+ "level": self.level.value,
81
+ "tool": self.tool,
82
+ "type": self.finding_type,
83
+ "message": self.message,
84
+ }
85
+ if self.counterexample is not None:
86
+ result["counterexample"] = self.counterexample
87
+ if self.suggestion is not None:
88
+ result["suggestion"] = self.suggestion
89
+ return result
90
+
91
+
92
+ @icontract.invariant(
93
+ lambda self: self.line >= 1,
94
+ "line number must be at least 1",
95
+ )
96
+ @icontract.invariant(
97
+ lambda self: is_non_empty_string(self.function),
98
+ "function name must be non-empty",
99
+ )
100
+ @icontract.invariant(
101
+ lambda self: is_non_empty_string(self.file),
102
+ "file path must be non-empty",
103
+ )
104
+ @dataclass(frozen=True)
105
+ class FunctionResult:
106
+ """Verification result for a single function.
107
+
108
+ Aggregates all findings across verification levels for one function.
109
+ """
110
+
111
+ function: str
112
+ file: str
113
+ line: int
114
+ level_requested: int
115
+ level_achieved: int
116
+ status: CheckStatus
117
+ details: tuple[Detail, ...] = ()
118
+
119
+ @icontract.ensure(
120
+ lambda result: isinstance(result, dict),
121
+ "result must be a dictionary",
122
+ )
123
+ def to_dict(self) -> dict[str, object]:
124
+ """Convert to a plain dictionary matching the JSON output spec."""
125
+ return {
126
+ "function": self.function,
127
+ "file": self.file,
128
+ "line": self.line,
129
+ "level_requested": self.level_requested,
130
+ "level_achieved": self.level_achieved,
131
+ "status": self.status.value,
132
+ "details": [d.to_dict() for d in self.details],
133
+ }
134
+
135
+
136
+ @icontract.invariant(
137
+ lambda self: is_non_negative_int(self.total_functions),
138
+ "total_functions must be non-negative",
139
+ )
140
+ @icontract.invariant(
141
+ lambda self: is_non_negative_int(self.passed_count),
142
+ "passed_count must be non-negative",
143
+ )
144
+ @icontract.invariant(
145
+ lambda self: is_non_negative_int(self.failed_count),
146
+ "failed_count must be non-negative",
147
+ )
148
+ @icontract.invariant(
149
+ lambda self: is_non_negative_int(self.skipped_count),
150
+ "skipped_count must be non-negative",
151
+ )
152
+ @icontract.invariant(
153
+ lambda self: is_non_negative_int(self.exempt_count),
154
+ "exempt_count must be non-negative",
155
+ )
156
+ @icontract.invariant(
157
+ lambda self: self.total_functions == self.passed_count + self.failed_count + self.skipped_count + self.exempt_count,
158
+ "counts must sum to total",
159
+ )
160
+ @dataclass(frozen=True)
161
+ class CheckSummary:
162
+ """Summary statistics for a verification run."""
163
+
164
+ total_functions: int
165
+ passed_count: int
166
+ failed_count: int
167
+ skipped_count: int
168
+ exempt_count: int = 0
169
+ duration_seconds: float = 0.0
170
+
171
+ @icontract.ensure(
172
+ lambda result: isinstance(result, dict),
173
+ "result must be a dictionary",
174
+ )
175
+ def to_dict(self) -> dict[str, object]:
176
+ """Convert to the summary dict matching the JSON output spec."""
177
+ return {
178
+ "total_functions": self.total_functions,
179
+ "passed": self.passed_count,
180
+ "failed": self.failed_count,
181
+ "skipped": self.skipped_count,
182
+ "exempt": self.exempt_count,
183
+ }
184
+
185
+
186
+ @icontract.invariant(
187
+ lambda self: self.level_achieved <= self.level_requested,
188
+ "level_achieved must not exceed level_requested",
189
+ )
190
+ @dataclass(frozen=True)
191
+ class CheckResult:
192
+ """Complete result of a verification run.
193
+
194
+ Contains the overall pass/fail status, all per-function results,
195
+ and summary statistics.
196
+ """
197
+
198
+ passed: bool
199
+ level_requested: int
200
+ level_achieved: int
201
+ results: tuple[FunctionResult, ...]
202
+ summary: CheckSummary
203
+ version: str = "0.1.0"
204
+
205
+ @property
206
+ def failures(self) -> list[FunctionResult]:
207
+ """Return only the failed function results."""
208
+ # Loop invariant: accumulated list contains only FAILED results seen so far
209
+ return [r for r in self.results if r.status == CheckStatus.FAILED]
210
+
211
+ @icontract.ensure(
212
+ lambda result: isinstance(result, dict),
213
+ "result must be a dictionary",
214
+ )
215
+ def to_dict(self) -> dict[str, object]:
216
+ """Convert to a plain dictionary matching the JSON output spec."""
217
+ return {
218
+ "version": self.version,
219
+ "passed": self.passed,
220
+ "level_requested": self.level_requested,
221
+ "level_achieved": self.level_achieved,
222
+ "summary": self.summary.to_dict(),
223
+ "results": [r.to_dict() for r in self.results],
224
+ }
225
+
226
+ @icontract.ensure(
227
+ lambda result: isinstance(result, str),
228
+ "result must be a string",
229
+ )
230
+ def to_json(self) -> str:
231
+ """Convert to a JSON string matching the spec output format."""
232
+ return json.dumps(self.to_dict(), indent=2)
233
+
234
+
235
+ @icontract.require(
236
+ lambda results: isinstance(results, tuple),
237
+ "results must be a tuple",
238
+ )
239
+ @icontract.ensure(
240
+ lambda result: isinstance(result, CheckResult),
241
+ "result must be a CheckResult",
242
+ )
243
+ def make_check_result(
244
+ results: tuple[FunctionResult, ...],
245
+ level_requested: int,
246
+ duration_seconds: float,
247
+ level_achieved: int | None = None,
248
+ ) -> CheckResult:
249
+ """Create a CheckResult from a tuple of FunctionResults.
250
+
251
+ Automatically computes passed/failed/skipped counts and overall status.
252
+
253
+ Args:
254
+ results: Tuple of per-function results.
255
+ level_requested: The verification level that was requested.
256
+ duration_seconds: How long the check took.
257
+ level_achieved: Optional aggregate level achieved override.
258
+
259
+ Returns:
260
+ A fully constructed CheckResult.
261
+ """
262
+ passed_count = 0
263
+ failed_count = 0
264
+ skipped_count = 0
265
+ exempt_count = 0
266
+ min_achieved = level_requested
267
+
268
+ # Loop invariant: counts reflect classifications of results[0..i]
269
+ for r in results:
270
+ if r.status == CheckStatus.EXEMPT:
271
+ exempt_count += 1
272
+ continue
273
+ if r.level_achieved < min_achieved:
274
+ min_achieved = r.level_achieved
275
+ if r.status == CheckStatus.PASSED:
276
+ passed_count += 1
277
+ elif r.status == CheckStatus.FAILED:
278
+ failed_count += 1
279
+ else:
280
+ skipped_count += 1
281
+
282
+ overall_level_achieved = (
283
+ min_achieved if level_achieved is None else level_achieved
284
+ )
285
+
286
+ summary = CheckSummary(
287
+ total_functions=len(results),
288
+ passed_count=passed_count,
289
+ failed_count=failed_count,
290
+ skipped_count=skipped_count,
291
+ exempt_count=exempt_count,
292
+ duration_seconds=duration_seconds,
293
+ )
294
+
295
+ # Exempt results are visible but do not block a passing result.
296
+ passed = (
297
+ failed_count == 0
298
+ and skipped_count == 0
299
+ and overall_level_achieved == level_requested
300
+ )
301
+
302
+ return CheckResult(
303
+ passed=passed,
304
+ level_requested=level_requested,
305
+ level_achieved=overall_level_achieved,
306
+ results=results,
307
+ summary=summary,
308
+ )
@@ -0,0 +1,6 @@
1
+ """Port definitions for Serenecode.
2
+
3
+ This package contains Protocol definitions (interfaces) that define
4
+ the boundaries between core domain logic and external I/O operations.
5
+ No implementations exist here — only abstract contracts.
6
+ """