proofside 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.
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from io import StringIO
6
+ from pathlib import Path
7
+ import tokenize
8
+
9
+
10
+ EQUATION_PREFIX = "# proofside equation:"
11
+ INTENT_PREFIX = "# proofside intent:"
12
+
13
+
14
+ class SpecificationAnnotationError(ValueError):
15
+ pass
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SpecificationAnnotations:
20
+ equations: tuple[str, ...]
21
+ intents: tuple[str, ...]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class AnnotatedFunction:
26
+ name: str
27
+ function: ast.FunctionDef
28
+ annotations: SpecificationAnnotations
29
+
30
+
31
+ def _comment_tokens(source: str) -> dict[int, tokenize.TokenInfo]:
32
+ lines = source.splitlines()
33
+ comment_tokens: dict[int, tokenize.TokenInfo] = {}
34
+ for token in tokenize.generate_tokens(StringIO(source).readline):
35
+ if token.type != tokenize.COMMENT:
36
+ continue
37
+ line_number, column = token.start
38
+ if lines[line_number - 1][:column].strip():
39
+ continue
40
+ comment_tokens[line_number] = token
41
+ return comment_tokens
42
+
43
+
44
+ def _annotations_for_function(
45
+ comment_tokens: dict[int, tokenize.TokenInfo],
46
+ function: ast.FunctionDef,
47
+ ) -> SpecificationAnnotations:
48
+ leading_comments = _leading_comments(comment_tokens, function)
49
+
50
+ equations = []
51
+ intents = []
52
+ for token in leading_comments:
53
+ for prefix, destination, label in (
54
+ (EQUATION_PREFIX, equations, "equation"),
55
+ (INTENT_PREFIX, intents, "intent"),
56
+ ):
57
+ if not token.string.startswith(prefix):
58
+ continue
59
+ text = token.string.removeprefix(prefix).strip()
60
+ if not text:
61
+ raise SpecificationAnnotationError(
62
+ f"empty proofside {label} annotation"
63
+ )
64
+ destination.append(text)
65
+ break
66
+
67
+ return SpecificationAnnotations(tuple(equations), tuple(intents))
68
+
69
+
70
+ def _leading_comments(
71
+ comment_tokens: dict[int, tokenize.TokenInfo],
72
+ function: ast.FunctionDef,
73
+ ) -> tuple[tokenize.TokenInfo, ...]:
74
+ leading_comments = []
75
+ line_number = function.lineno - 1
76
+ while line_number in comment_tokens:
77
+ token = comment_tokens[line_number]
78
+ if token.start[1] != function.col_offset:
79
+ break
80
+ leading_comments.append(token)
81
+ line_number -= 1
82
+ leading_comments.reverse()
83
+ return tuple(leading_comments)
84
+
85
+
86
+ def specification_annotations_for_function(
87
+ source: str,
88
+ function: ast.FunctionDef,
89
+ ) -> SpecificationAnnotations:
90
+ return _annotations_for_function(_comment_tokens(source), function)
91
+
92
+
93
+ def marked_functions_in_source(source: str) -> tuple[ast.FunctionDef, ...]:
94
+ tree = ast.parse(source)
95
+ comment_tokens = _comment_tokens(source)
96
+ prefixes = (EQUATION_PREFIX, INTENT_PREFIX)
97
+ return tuple(
98
+ function
99
+ for function in tree.body
100
+ if isinstance(function, ast.FunctionDef)
101
+ and any(
102
+ token.string.startswith(prefixes)
103
+ for token in _leading_comments(comment_tokens, function)
104
+ )
105
+ )
106
+
107
+
108
+ def extract_specification_annotations(source: str) -> tuple[AnnotatedFunction, ...]:
109
+ tree = ast.parse(source)
110
+ comment_tokens = _comment_tokens(source)
111
+
112
+ discovered = []
113
+ for function in (
114
+ node for node in tree.body if isinstance(node, ast.FunctionDef)
115
+ ):
116
+ annotations = _annotations_for_function(comment_tokens, function)
117
+ if annotations.equations or annotations.intents:
118
+ discovered.append(
119
+ AnnotatedFunction(
120
+ function.name,
121
+ function,
122
+ annotations,
123
+ )
124
+ )
125
+ return tuple(discovered)
126
+
127
+
128
+ def find_annotated_functions(file_path: Path) -> tuple[AnnotatedFunction, ...]:
129
+ return extract_specification_annotations(file_path.read_text(encoding="utf-8"))
@@ -0,0 +1,373 @@
1
+ Metadata-Version: 2.4
2
+ Name: proofside
3
+ Version: 0.1.0
4
+ Summary: A small formal-verification sidecar for typed mathematical Python
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Repository, https://github.com/kli0n2323/proofside
7
+ Requires-Python: <3.15,>=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: nagini==1.3.1
11
+ Dynamic: license-file
12
+
13
+ # Proofside
14
+
15
+ **State the math. Review the contract. Verify the code.**
16
+
17
+ Proofside is a small formal-verification sidecar for typed mathematical Python.
18
+ Declare the math a function is supposed to implement, review an explicit
19
+ contract, then ask Nagini/Viper whether the implementation satisfies it.
20
+
21
+ Proofside annotations keep equations and plain-language intent beside the code
22
+ but separate from the implementation body. An optional model can translate that
23
+ declared specification into an untrusted candidate contract without seeing the
24
+ body by default. No model is required: contracts may also be authored manually,
25
+ and a model never determines whether a proof succeeds.
26
+
27
+ ## Quick start
28
+
29
+ Proofside supports 64-bit Python 3.12–3.14 and requires a 64-bit Java 11+
30
+ runtime. From a checkout:
31
+
32
+ ```bash
33
+ python -m venv .venv
34
+ # Linux/macOS: source .venv/bin/activate
35
+ # Windows PowerShell: .venv\Scripts\Activate.ps1
36
+ python -m pip install .
37
+ ```
38
+
39
+ Start with a reproducible model-free verification:
40
+
41
+ ```bash
42
+ proofside check examples/sidecar/shot_budget_plain.py::allocate_remaining \
43
+ --contract examples/sidecar/shot_budget_contract.json
44
+ ```
45
+
46
+ The contract assumes nonnegative counts with `first_bucket <= total_shots` and
47
+ guarantees a nonnegative result that conserves the declared budget. The command
48
+ prints the contract, `VERIFIED`, and an explicit proof boundary.
49
+
50
+ Now check the nearly identical broken implementation:
51
+
52
+ ```bash
53
+ proofside check examples/sidecar/shot_budget_plain_bad.py::allocate_remaining \
54
+ --contract examples/sidecar/shot_budget_contract.json
55
+ ```
56
+
57
+ Nagini reports `FAILED` because the extra `+ 1` prevents budget conservation.
58
+ Prefixing commands with `python -m proofside` is also supported.
59
+
60
+ ## Examples
61
+
62
+ | Path | Purpose |
63
+ | --- | --- |
64
+ | `examples/sidecar/` | Model-free Proofside JSON contract workflow: good and bad implementations share one explicit contract. |
65
+ | `examples/nagini/` | Direct handwritten Nagini contracts. Proofside supports this path but does not parse Nagini annotations back into its IR. |
66
+ | `examples/annotated/shot_budget_annotated.py` | Smallest annotation-first specification and proposal example. |
67
+ | `examples/annotated/model_workflow_stress.py` | Eight-function model-assisted stress fixture with both matching and intentionally mismatched implementations. Its annotations are normative; bodies are withheld by default. |
68
+ | `examples/research/` | Research-derived bookkeeping example with careful, limited provenance. |
69
+ | `examples/unsupported/` | A source boundary that Proofside explicitly reports as unsupported. |
70
+
71
+ ## Declare the intended math
72
+
73
+ Proofside recognizes two case-sensitive comments in the contiguous comment
74
+ block immediately above a supported function:
75
+
76
+ ```python
77
+ # proofside equation: result = total_shots - first_bucket
78
+ # proofside intent: Return the unallocated portion of the declared budget.
79
+ def allocate_remaining(total_shots: int, first_bucket: int) -> int:
80
+ return total_shots - first_bucket
81
+ ```
82
+
83
+ Equation and intent text is opaque, user-authored specification material.
84
+ Proofside preserves it rather than interpreting, normalizing, or checking the
85
+ mathematics at annotation-extraction time. Multiple equation or intent lines
86
+ are allowed.
87
+
88
+ The specification says what the function is supposed to mean; the implementation
89
+ is the object later checked against the accepted contract. They have different
90
+ roles.
91
+
92
+ ## From specification to proof
93
+
94
+ ```text
95
+ user-declared specification
96
+ equation / intent / selected sources
97
+ ↓
98
+ optional model translation
99
+ ↓
100
+ candidate contract
101
+ NOT VERIFIED
102
+ ↓
103
+ human review/edit
104
+ ↓
105
+ accepted contract ─────────┐
106
+ NOT VERIFIED │
107
+ ├→ selected explicit contract
108
+ manually authored JSON contract ─────────┘
109
+ NOT VERIFIED
110
+ +
111
+ implementation under test
112
+ ↓
113
+ deterministic Proofside lowering
114
+ ↓
115
+ Nagini / Viper / Z3
116
+ ↓
117
+ VERIFIED / FAILED / UNSUPPORTED / ERROR
118
+ ```
119
+
120
+ Handwritten Nagini contracts provide a lower-level route directly to Nagini.
121
+ In every route, Nagini/Viper—not a model—decides whether proof obligations are
122
+ discharged.
123
+
124
+ To ask an explicitly selected model for a candidate:
125
+
126
+ ```bash
127
+ proofside propose examples/annotated/shot_budget_annotated.py::allocate_remaining \
128
+ --model-source api \
129
+ --model MODEL_NAME
130
+ ```
131
+
132
+ Without `--out`, this writes:
133
+
134
+ ```text
135
+ examples/annotated/.proofside/shot_budget_annotated.allocate_remaining.candidate.json
136
+ ```
137
+
138
+ The response must be exact JSON. Proofside strictly parses and structurally
139
+ validates it, but labels it `PROPOSED — NOT VERIFIED`. Review or edit that file,
140
+ then record the explicit choice to submit it for verification:
141
+
142
+ ```bash
143
+ proofside accept examples/annotated/shot_budget_annotated.py::allocate_remaining
144
+ ```
145
+
146
+ This retains the candidate and creates:
147
+
148
+ ```text
149
+ examples/annotated/.proofside/shot_budget_annotated.allocate_remaining.contract.json
150
+ ```
151
+
152
+ Acceptance means only “accepted for verification.” It neither asserts that the
153
+ contract is correct nor runs the verifier. Verification remains a separate
154
+ action:
155
+
156
+ ```bash
157
+ proofside check examples/annotated/shot_budget_annotated.py::allocate_remaining \
158
+ --contract examples/annotated/.proofside/shot_budget_annotated.allocate_remaining.contract.json
159
+ ```
160
+
161
+ An explicit `--out custom.json` remains available for proposal, and
162
+ `accept --candidate custom.json` validates a reviewed custom candidate into the
163
+ deterministic accepted path. `accept --replace` is required to replace an
164
+ existing accepted contract, and validation completes before replacement.
165
+
166
+ No model is needed for either contract route. A manually authored JSON contract
167
+ may be checked directly with `check --contract`; it does not have to pass through
168
+ `accept`. Advanced users may instead keep handwritten Nagini annotations in the
169
+ function:
170
+
171
+ ```bash
172
+ proofside check examples/nagini/shot_budget_good.py::allocate_remaining
173
+ ```
174
+
175
+ Proofside does not parse handwritten Nagini annotations into its contract IR.
176
+
177
+ ## Specification sources
178
+
179
+ `propose` and `propose-all` accept repeatable `--source` choices:
180
+
181
+ - `equation` — the function's `proofside equation` lines;
182
+ - `intent` — the function's `proofside intent` lines;
183
+ - `implementation` — the selected function body, only when explicitly chosen.
184
+
185
+ With no `--source`, Proofside uses every available annotation type in the fixed
186
+ order `equation`, then `intent`. It never silently falls back to the
187
+ implementation. An unannotated function therefore requires explicit
188
+ `--source implementation`; sources may also be combined.
189
+
190
+ The function name, parameter names and types, and return annotation remain
191
+ structural context. Unless `implementation` is selected, the model does not
192
+ receive the body, return expression, neighboring functions, imports, tests, or
193
+ other repository content.
194
+
195
+ ## Candidate and accepted artifacts
196
+
197
+ For `path/module.py::function`, the source-adjacent convention is:
198
+
199
+ ```text
200
+ path/.proofside/module.function.candidate.json
201
+ proposed or supplied for review; unaccepted; NOT VERIFIED
202
+
203
+ path/.proofside/module.function.contract.json
204
+ explicitly accepted for verification; still NOT VERIFIED
205
+ ```
206
+
207
+ Both files contain ordinary Proofside contract JSON without approval metadata.
208
+ Proposal never overwrites an existing candidate, accepts it, or verifies it.
209
+ Acceptance validates before writing, never calls a model or verifier, and leaves
210
+ the candidate in place.
211
+
212
+ ## Batch workflow
213
+
214
+ Batch commands are deterministic orchestration over independent single-function
215
+ operations. Only marked top-level functions participate; there is no batch
216
+ contract or multi-function proof.
217
+
218
+ ```bash
219
+ proofside propose-all examples/annotated/ --model-source api --model MODEL_NAME
220
+ ```
221
+
222
+ `propose-all` may make one sequential model request per marked function needing
223
+ a candidate. It uses each function's equation/intent annotations by default and
224
+ withholds each body unless `--source implementation` is selected. Candidates
225
+ are written independently; none is accepted or verified.
226
+
227
+ Review the candidate files and accept each chosen contract explicitly:
228
+
229
+ ```bash
230
+ proofside accept research/budget.py::remaining
231
+ ```
232
+
233
+ Then verify all marked functions with accepted source-adjacent contracts:
234
+
235
+ ```bash
236
+ proofside check-all research/
237
+ ```
238
+
239
+ One file or function failure does not stop later independent operations. Both
240
+ batch commands recursively inspect only the supplied files/directories, prune
241
+ hidden directories, deduplicate paths, and use deterministic ordering.
242
+
243
+ The conspicuous bypass below lets `check-all` fall back to a candidate only when
244
+ no accepted contract exists:
245
+
246
+ ```bash
247
+ proofside check-all research/ --allow-unreviewed
248
+ ```
249
+
250
+ Candidate-backed results are labeled `UNREVIEWED CONTRACT`, and the overall
251
+ exit remains nonzero even if every underlying proof verifies. An accepted
252
+ contract always wins over a candidate.
253
+
254
+ ## What a result means
255
+
256
+ - `VERIFIED`: Nagini/Viper discharged the displayed proof obligations.
257
+ - `FAILED`: one or more obligations were not proved. This does not by itself
258
+ mean that a concrete counterexample was produced.
259
+ - `UNSUPPORTED`: Proofside recognized input outside its deliberately narrow
260
+ source or contract boundary.
261
+ - `ERROR`: malformed input, missing tooling, or another setup/runtime problem
262
+ prevented meaningful verification.
263
+
264
+ `UNSUPPORTED` and `ERROR` do not imply that verification occurred.
265
+
266
+ A successful run establishes that the selected implementation satisfies the
267
+ displayed guarantees under the displayed assumptions, within the supported
268
+ Nagini/Viper semantics. Preconditions remain obligations on callers.
269
+
270
+ Formal verification does not establish that the contract captures the intended
271
+ mathematics, that a scientific model corresponds to reality, that an algorithm
272
+ is useful or optimal, or that an experiment is empirically valid. The researcher
273
+ remains responsible for the specification and its scientific meaning.
274
+
275
+ ## Supported boundary
276
+
277
+ The closed contract language supports:
278
+
279
+ - arithmetic values: parameters, integer literals, the result in
280
+ postconditions, addition, subtraction, negation, and multiplication by an
281
+ integer constant;
282
+ - logical formulas: `==`, `!=`, `<`, `<=`, `>`, `>=`, `and`, `or`, `not`, and
283
+ implication;
284
+ - lists of preconditions (`requires`) and postconditions (`ensures`).
285
+
286
+ This is intentionally close to quantifier-free propositional linear integer
287
+ arithmetic. It excludes arbitrary variable-by-variable multiplication,
288
+ division, powers, quantifiers, arrays, floating point, and arbitrary Python
289
+ expressions.
290
+
291
+ For example, piecewise intent can be preserved directly:
292
+
293
+ ```text
294
+ value >= 0 -> result == value
295
+ value < 0 -> result == 0
296
+ ```
297
+
298
+ That says more than weakening the function to global bounds such as
299
+ `result >= 0`.
300
+
301
+ It contains no raw Python or Nagini snippets.
302
+
303
+ Proofside currently selects top-level synchronous functions with complete
304
+ parameter and return annotations. Decorated, async, nested, ambiguous, or
305
+ untyped targets are rejected. Sidecar mode also rejects function docstrings and
306
+ one-line bodies.
307
+
308
+ The selected function must be self-contained. Sidecar source generation does
309
+ not preserve arbitrary imports, helpers, closures, comments, or module state.
310
+ Proofside does not provide verification semantics for arbitrary Python, NumPy,
311
+ SciPy, or research frameworks.
312
+
313
+ ## Trust and model transport
314
+
315
+ Proofside strictly parses one closed contract representation, validates names
316
+ against the selected function, and lowers accepted JSON deterministically to a
317
+ temporary Nagini source. Nagini, Viper/Silicon, and Z3 perform formal
318
+ verification. Temporary verifier sources are removed after each run.
319
+
320
+ Model output and annotation payloads are untrusted input. A model receives only
321
+ the selected specification sources plus structural signature context:
322
+
323
+ - API mode sends that context remotely. The default OpenAI endpoint alone reads
324
+ `OPENAI_API_KEY` implicitly.
325
+ - A custom API endpoint must use HTTPS and explicitly name its credential with
326
+ `--api-key-env`; Proofside does not forward `OPENAI_API_KEY` automatically.
327
+ - Local mode defaults to `http://localhost:11434/v1` and is unauthenticated.
328
+ - Authenticated requests refuse every HTTP redirect so credentials cannot be
329
+ forwarded to a redirect target.
330
+
331
+ Proofside makes one non-streaming request with a finite timeout and no automatic
332
+ retry, repair, or verifier-feedback loop.
333
+
334
+ ## Research example
335
+
336
+ ```bash
337
+ proofside check examples/research/research_shot_budget.py::remaining_feature_shots \
338
+ --contract examples/research/research_shot_budget_contract.json
339
+ ```
340
+
341
+ This synthetic bookkeeping kernel is inspired by two motifs in the author's
342
+ research workflow: finite-shot Pauli feature acquisition and train/test
343
+ separation. It is not copied from that implementation and does not represent a
344
+ canonical allocation policy. The private research repository is not needed to
345
+ understand or run this self-contained example.
346
+
347
+ The contract proves a nonnegative remainder and conservation of the declared
348
+ counts under its assumptions. It does not prove that the split is optimal, that
349
+ the acquisition choices are scientifically appropriate, that the method
350
+ improves a downstream task, or that a policy transfers to hardware.
351
+
352
+ ## Development
353
+
354
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup, tests, the code map, and
355
+ design constraints. If Nagini cannot find Java—especially on Windows—set
356
+ `JAVA_HOME` to the Java installation root.
357
+
358
+ See [`ROADMAP.md`](ROADMAP.md) for suggested contribution directions.
359
+
360
+ ## Third-party software and license status
361
+
362
+ | Software | Version | License | Use here |
363
+ | --- | --- | --- | --- |
364
+ | [Nagini](https://github.com/marcoeilers/nagini) | 1.3.1 | MPL-2.0 | Direct pinned verification dependency |
365
+ | [Viper/Silicon](https://github.com/viperproject/silicon) | Bundled by Nagini 1.3.1 | MPL-2.0 | Verification backend used through Nagini |
366
+ | [Z3](https://github.com/Z3Prover/z3) | 4.8.7.0 on common x64 platforms | MIT | Solver installed transitively by Nagini |
367
+
368
+ Proofside depends on these projects but contains no copied or adapted
369
+ third-party source. Python and Java are execution prerequisites distributed
370
+ under their respective licenses.
371
+
372
+ Proofside is licensed under the Apache License 2.0. See [`LICENSE`](LICENSE).
373
+
@@ -0,0 +1,15 @@
1
+ proofside/__init__.py,sha256=gS0jlO3xE9XYicoQYzF_2cRYWeUT-m4UCQiPSVktbm8,69
2
+ proofside/__main__.py,sha256=nbsrlOGKG0yYHkJ3aEvJMIm3ouChJxGNJz_Jp3I-1gE,82
3
+ proofside/acceptance.py,sha256=4nTlxBCK1SBjMAajYvHAeM8EMGVrLXOrQPZnMffKalQ,2958
4
+ proofside/artifacts.py,sha256=mWGtZxs80ZHwT7OqvIYCEGknxrugFjaH_GoWry5EnEA,367
5
+ proofside/batch.py,sha256=dVjcphCUnIyQ6hUpx5lTj8PYVmOrTJdoV5hscfjjXeE,10026
6
+ proofside/cli.py,sha256=uAWP8EJUSDLByvo3dHN0Lpu7GTuHTtdTES7K2E4RuD0,16294
7
+ proofside/contracts.py,sha256=4TPe3xDZe4SVae0m_UKT4IhZA1UQcbhdp4qA5fjmxOs,13385
8
+ proofside/proposal.py,sha256=AHbhjaYQ-7vapLfMQxUBasZEp8fWMPeBulbFupV_I9Q,11654
9
+ proofside/specification.py,sha256=BchpZB6iU9Ym1GGdpMr1h8zB4nWq-gyz6Cw2plqYynE,3835
10
+ proofside-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
11
+ proofside-0.1.0.dist-info/METADATA,sha256=P7mARcvu_R_SxlQC7LcQNMBoMr3me5p0H9UXdLnu5zs,14879
12
+ proofside-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ proofside-0.1.0.dist-info/entry_points.txt,sha256=OeE6fCFRsqT72CS6pI0afJcI-MSEzV4A8qzd8zf7qDE,49
14
+ proofside-0.1.0.dist-info/top_level.txt,sha256=kO57OCmJayhZlqnrlqOhGlQDQz7YgflBYCbbZHD1mZs,10
15
+ proofside-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ proofside = proofside.cli:main