codecalc 0.1.0__py3-none-win_amd64.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.
codecalc/SKILL.md ADDED
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: codecalc
3
+ description: Use when about to state a number, claim two programs are equivalent, claim a speedup, or state a complexity — codecalc computes these exactly instead of estimating them, and this file says when calling is mandatory and how results must be reported.
4
+ ---
5
+
6
+ # codecalc
7
+
8
+ You are a language model. You are good at writing code and bad at arithmetic,
9
+ and the failure is not that you are unsure — it is that you are confident. This
10
+ file exists because "call a tool when you are uncertain" is worthless advice to
11
+ a model that feels certain about `0.1 + 0.2`.
12
+
13
+ So the rules below key on the SITUATION, never on how sure you feel.
14
+
15
+ ## Call these — mandatory, no exceptions
16
+
17
+ Not a preference. If the situation matches, the call happens before you write
18
+ the number.
19
+
20
+ | Situation | Tool |
21
+ |---|---|
22
+ | Any **non-integer** operand or result | `calc_exact` |
23
+ | Any **comparison** whose verdict you will state | `compare_threshold` |
24
+ | Integers past **2^53** (9007199254740992) | `calc_exact`, and `float_repr` if a float is involved |
25
+ | Percentages, ratios, shares | `percentage` |
26
+ | Unit, byte, duration or epoch conversion | `convert_units`, `data_sizes`, `human_duration`, `epoch_time` |
27
+ | Any number that appears in your answer **as a claim** rather than an illustration | whichever of the above fits |
28
+ | You ported code A → B and are about to call it a port | `verify_translation` |
29
+ | You are about to state that a rewrite is **faster** | `verify_optimization` |
30
+ | You are about to say two languages **behave the same** | `compare_edge_cases` |
31
+ | You are about to state a **Big-O** | `analyze_complexity` (inferred) or `benchmark` (measured) — and say which |
32
+
33
+ **2^53 is not academic.** `float_repr(9007199254740993)` reports
34
+ `stored: "9007199254740992"`. The value changed and nothing raised. That is the
35
+ boundary, and `int_widths` will not show it to you — that tool reports machine
36
+ integer widths (i8…u64), which is a different question.
37
+
38
+ ## Do not call these — no justification needed
39
+
40
+ Calling a tool for these is noise, and a rule that fires constantly is a rule
41
+ someone turns off:
42
+
43
+ - small-integer intermediate arithmetic — `2 + 3 + 4` needs no tool
44
+ - order-of-magnitude estimates you label as estimates
45
+ - arithmetic inside prose that nobody will act on
46
+ - `execute_code` to confirm syntax you can read
47
+
48
+ Operand count is **not** the test. `0.1 + 0.2` has two operands and is the
49
+ canonical failure; `2 + 3 + 4` has three and is never wrong. Type predicts
50
+ error. Length does not.
51
+
52
+ ## Report what came back — mandatory, no exceptions
53
+
54
+ This is the second-order failure: call the tool correctly, then misreport it.
55
+ Every field below exists because the result would otherwise imply something
56
+ untrue, and each was added after that implication bit someone.
57
+
58
+ | Field | What you must say |
59
+ |---|---|
60
+ | `passed: true` | State `total`. "Equivalent on 7 inputs" — never "verified equivalent" |
61
+ | `inconclusive > 0` | Surface it. Not a pass, not a failure, and the caller is entitled to know which they got |
62
+ | `unenforced: [...]` non-empty | The guarantee you are about to describe **did not hold**. Name what was not applied |
63
+ | `backend: "python"` | Weaker sandbox. `no_net` was not applied and `peak_memory_kb` is `None` |
64
+ | `analysis: "regex-fallback"` | The structure was **guessed**, not parsed |
65
+ | `method: "static-estimate"` | Nobody measured this. `benchmark` reports `method: "empirical"` |
66
+ | `output_error` present | `stdout`/`stderr` are **not** what the program produced |
67
+ | `ok: false` | Say what failed. The `error` field is written to be quoted |
68
+
69
+ **Never drop a field you do not understand. Quote it.**
70
+
71
+ That rule is not for careless models. In one session an agent verifying this
72
+ very codebase — carefully, with the source open — made three probe errors in a
73
+ row: reversed arguments that made a nested loop report `O(1)`, a read of a
74
+ `divergences` key that does not exist (it is `mismatched`), and the same
75
+ wrong-key mistake on `algebraic_equiv` (it is `identical`). Each was caught only
76
+ by printing the whole result instead of the field it expected. Confidence about
77
+ a result's shape is exactly as unreliable as confidence about arithmetic.
78
+
79
+ ## What this tool set will not do for you
80
+
81
+ - It does not check that a package or function **exists**. A fabricated API is
82
+ still your error; `execute_code` with an import is the closest available check.
83
+ - `verify_translation` proves equivalence **on the inputs it ran**, which are
84
+ `DEFAULT_EDGE_INPUTS` unless you pass your own. It is evidence, not proof.
85
+ Pass inputs that would distinguish your port if it were wrong.
86
+ - `analyze_complexity` reads structure. It cannot see work hidden inside a
87
+ library call.
codecalc/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """codecalc — universal code & logic calculator for AI models."""
2
+
3
+ __version__ = "0.1.0"
codecalc/__main__.py ADDED
@@ -0,0 +1,19 @@
1
+ """`python -m codecalc` — the same entry point as the `codecalc` script.
2
+
3
+ `[project.scripts]` already gives `codecalc` a console script pointing at
4
+ `server:main`, so `uvx codecalc` and `pipx run codecalc` have always worked.
5
+ What did not work was `python -m codecalc`, which needs this file and failed
6
+ with "No module named codecalc.__main__" — the form people reach for when a
7
+ package is installed but its script directory is not on PATH, which is the
8
+ normal state inside a venv someone has not activated.
9
+
10
+ Deliberately thin. Anything that lives here is invisible to the console-script
11
+ path and would be a second way for the two to diverge.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .server import main
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,211 @@
1
+ """Stateful python3 REPL worker: JSON-lines request/response over a channel
2
+ executed code cannot reach.
3
+
4
+ Three things here are deliberate and were all bugs before.
5
+
6
+ **The protocol does not share a file descriptor with the executed code.** The
7
+ first version swapped `sys.stdout` for a StringIO and wrote responses with
8
+ `print()`. That is a Python-level rebind: a subprocess inherits **fd 1** and
9
+ writes straight past it. So an entirely ordinary session line —
10
+
11
+ subprocess.run(["echo", "hi"])
12
+
13
+ — put raw bytes into the response stream. At startup fd 1 is duplicated to a
14
+ private descriptor that only this module writes to, and fd 1/2 are then pointed
15
+ at capture files. Nothing the executed code can do reaches the protocol channel,
16
+ and as a bonus subprocess output is now CAPTURED rather than lost.
17
+
18
+ **On Windows, the fd-1 dup above is not trusted, and a file stands in for it.**
19
+ `os.dup2` remaps this process's CRT file-descriptor table, but a subprocess
20
+ started by executed code without an explicit stdout override can still be
21
+ handed the OS-level standard HANDLE instead — a separate piece of state the
22
+ CRT does not reliably keep in sync with the fd table on Windows. When
23
+ `CODECALC_PROTO_PATH` is set (Windows, or `_FORCE_FILE_PROTOCOL` in
24
+ sessions.py forcing the same route on POSIX for test coverage), responses are
25
+ appended to that file instead of written to the fd-1 dup, exactly mirroring
26
+ the node worker's Windows route (`sessions.py`'s `_TailReader`). The per-call
27
+ fds are additionally mirrored onto the OS-level standard handles there (see
28
+ `_set_std_handle` below), so a child that queries the raw handle rather than
29
+ the CRT fd still lands in the capture file, not wherever fd 1 used to point.
30
+
31
+ **Every response carries the id of the request it answers.** Corruption used to
32
+ be unrecoverable *and silent*: the reader consumed the junk, left the real reply
33
+ queued, and every later call returned the PREVIOUS call's result with
34
+ `ok: True`. Measured — a call asking for `marker-4` came back with the output of
35
+ the call before it. An echoed id makes a desync detectable instead of trusted;
36
+ see `Worker.run`, which kills the worker rather than return a stale answer.
37
+
38
+ Output is capped here too. The sandboxed path caps at 64 KiB and reports OLE;
39
+ this path returned a 20 MB string whole, built three times over in memory on the
40
+ way out through JSON.
41
+ """
42
+
43
+ import contextlib
44
+ import io
45
+ import json
46
+ import os
47
+ import sys
48
+ import tempfile
49
+ import traceback
50
+ from pathlib import Path
51
+
52
+ #: Matches MAX_OUTPUT_BYTES in the Rust executor. A session is not exempt from
53
+ #: the output cap just because it is stateful.
54
+ MAX_OUTPUT_BYTES = 64 * 1024
55
+
56
+ # The protocol channel. Two routes:
57
+ #
58
+ # CODECALC_PROTO_PATH set responses are appended to that file. Set on
59
+ # Windows (and by sessions.py's
60
+ # _FORCE_FILE_PROTOCOL, on POSIX, for test
61
+ # coverage) because the fd-1 dup below is not
62
+ # trustworthy there.
63
+ # unset (default, POSIX) a dup of the ORIGINAL stdout, taken before
64
+ # anything is redirected. Executed code cannot
65
+ # name this descriptor, and closing fd 1 (which
66
+ # code is free to do) does not touch it.
67
+ _proto_path = os.environ.get("CODECALC_PROTO_PATH", "")
68
+ _proto_path_obj = Path(_proto_path) if _proto_path else None
69
+ if _proto_path:
70
+ _proto = None
71
+ else:
72
+ _proto_fd = os.dup(1)
73
+ _proto = os.fdopen(_proto_fd, "w", encoding="utf-8", newline="\n")
74
+
75
+
76
+ def _write_proto(line: str) -> None:
77
+ if _proto_path_obj is not None:
78
+ # Append, not write: a fresh open() per response so the file descriptor
79
+ # itself is never held across the exec() below, where executed code
80
+ # could otherwise inherit or close it.
81
+ with _proto_path_obj.open("a", encoding="utf-8", newline="\n") as fh:
82
+ fh.write(line)
83
+ else:
84
+ _proto.write(line)
85
+ _proto.flush()
86
+
87
+
88
+ # On Windows, mirror the per-call fd 1/2 redirection onto the OS-level
89
+ # standard handles too. `os.dup2` only remaps this process's CRT
90
+ # file-descriptor table; a child spawned by executed code without an explicit
91
+ # stdout override can still be handed GetStdHandle()'s value instead, which
92
+ # the CRT does not reliably keep pointed at the same place. Elsewhere this is
93
+ # a no-op: the fd-1 dup2 is already sufficient on POSIX.
94
+ _STD_OUTPUT_HANDLE = -11
95
+ _STD_ERROR_HANDLE = -12
96
+
97
+ if os.name == "nt":
98
+ import ctypes
99
+ import msvcrt
100
+
101
+ _kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
102
+
103
+ def _set_std_handle(std_id: int, fd: int) -> int:
104
+ """Point the OS-level standard handle at fd's handle; return the old one."""
105
+ prev = _kernel32.GetStdHandle(std_id)
106
+ _kernel32.SetStdHandle(std_id, msvcrt.get_osfhandle(fd))
107
+ return prev
108
+
109
+ def _restore_std_handle(std_id: int, handle: int) -> None:
110
+ _kernel32.SetStdHandle(std_id, handle)
111
+ else:
112
+ def _set_std_handle(std_id: int, fd: int) -> int:
113
+ return -1
114
+
115
+ def _restore_std_handle(std_id: int, handle: int) -> None:
116
+ pass
117
+
118
+
119
+ # Requests still arrive on stdin. The iterator below binds to this object once,
120
+ # so rebinding sys.stdin per call cannot disturb it.
121
+ _requests = sys.stdin
122
+
123
+ ns = {"__name__": "__main__", "__builtins__": __builtins__}
124
+
125
+
126
+ def _read_capped(fh) -> tuple[str, bool]:
127
+ """Read a capture file, capped, reporting whether anything was dropped."""
128
+ fh.seek(0)
129
+ data = fh.read(MAX_OUTPUT_BYTES + 1)
130
+ truncated = len(data) > MAX_OUTPUT_BYTES
131
+ if truncated:
132
+ data = data[:MAX_OUTPUT_BYTES]
133
+ return data.decode("utf-8", errors="replace") + ("\n...[truncated]" if truncated else ""), truncated
134
+
135
+
136
+ def _respond(payload: dict) -> None:
137
+ _write_proto(json.dumps(payload) + "\n")
138
+
139
+
140
+ for _line in _requests:
141
+ _line = _line.strip()
142
+ if not _line:
143
+ continue
144
+ try:
145
+ req = json.loads(_line)
146
+ except Exception:
147
+ continue
148
+ code = req.get("code", "")
149
+ stdin_data = req.get("stdin", "")
150
+ req_id = req.get("id")
151
+
152
+ # Not a `with` block: these files have to outlive the try/finally that
153
+ # restores fds 1 and 2, and they are read after the restore. Closed
154
+ # explicitly below.
155
+ out_f = tempfile.TemporaryFile() # noqa: SIM115
156
+ err_f = tempfile.TemporaryFile() # noqa: SIM115
157
+ # Save fds 1/2 so they can be restored even if the executed code closes them.
158
+ saved_out, saved_err = os.dup(1), os.dup(2)
159
+ prev_stdin, prev_stdout, prev_stderr = sys.stdin, sys.stdout, sys.stderr
160
+ saved_std_out_handle = saved_std_err_handle = None
161
+ try:
162
+ os.dup2(out_f.fileno(), 1)
163
+ os.dup2(err_f.fileno(), 2)
164
+ # See the module docstring: on Windows the CRT fd redirection above
165
+ # does not, by itself, reach a child spawned without an explicit
166
+ # stdout/stderr override. Mirroring it onto the OS-level standard
167
+ # handles is what makes THAT child land in the capture files too.
168
+ saved_std_out_handle = _set_std_handle(_STD_OUTPUT_HANDLE, 1)
169
+ saved_std_err_handle = _set_std_handle(_STD_ERROR_HANDLE, 2)
170
+ # Python-level streams over the SAME descriptors, so interpreter output
171
+ # and subprocess output interleave in the order they were written.
172
+ sys.stdout = io.TextIOWrapper(os.fdopen(os.dup(1), "wb"), encoding="utf-8",
173
+ errors="replace", write_through=True)
174
+ sys.stderr = io.TextIOWrapper(os.fdopen(os.dup(2), "wb"), encoding="utf-8",
175
+ errors="replace", write_through=True)
176
+ sys.stdin = io.StringIO(stdin_data)
177
+ with contextlib.redirect_stdout(sys.stdout), contextlib.redirect_stderr(sys.stderr):
178
+ exec(compile(code, "<session>", "exec"), ns)
179
+ ok, err = True, ""
180
+ except BaseException:
181
+ # BaseException, not Exception: sys.exit() in a session line must end
182
+ # that line, not the worker.
183
+ ok, err = False, traceback.format_exc()
184
+ finally:
185
+ for stream in (sys.stdout, sys.stderr):
186
+ with contextlib.suppress(Exception):
187
+ stream.flush()
188
+ if saved_std_out_handle is not None:
189
+ _restore_std_handle(_STD_OUTPUT_HANDLE, saved_std_out_handle)
190
+ if saved_std_err_handle is not None:
191
+ _restore_std_handle(_STD_ERROR_HANDLE, saved_std_err_handle)
192
+ os.dup2(saved_out, 1)
193
+ os.dup2(saved_err, 2)
194
+ os.close(saved_out)
195
+ os.close(saved_err)
196
+ sys.stdin, sys.stdout, sys.stderr = prev_stdin, prev_stdout, prev_stderr
197
+
198
+ stdout_s, out_trunc = _read_capped(out_f)
199
+ stderr_s, err_trunc = _read_capped(err_f)
200
+ out_f.close()
201
+ err_f.close()
202
+
203
+ _respond({
204
+ "id": req_id,
205
+ "ok": ok,
206
+ "stdout": stdout_s,
207
+ "stderr": stderr_s or err,
208
+ "exit_code": 0 if ok else 1,
209
+ "output_truncated": out_trunc or err_trunc,
210
+ "verdict": "OLE" if (out_trunc or err_trunc) else ("OK" if ok else "RTE"),
211
+ })
Binary file
codecalc/complexity.py ADDED
@@ -0,0 +1,162 @@
1
+ """Complexity analyzer: static heuristic Big-O estimation.
2
+
3
+ Language-agnostic structural scan:
4
+ - loop constructs per nesting depth -> O(n^k) base
5
+ - recursion detection -> exponential / nlogn flags
6
+ - linear-scan builtins (sort etc.) -> n log n adjustments
7
+ - hash/constant-time ops -> n^1 stays n
8
+
9
+ Structural only: a tree-sitter parse plus growth heuristics. There is no LLM
10
+ in this path and no network call. An optional LLM "second opinion" used to sit
11
+ behind CODECALC_COMPLEXITY_LLM; it is gone, because a static analyser that can
12
+ make a network call is a different kind of tool, and the deterministic estimate
13
+ was always the product.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+
20
+ from . import parsing
21
+
22
+ LOOP_RE = re.compile(
23
+ r"\b(for|while|foreach|until|loop|repeat)\b"
24
+ r"|\bfor\s*\(" # C-style for(
25
+ r"|\.forEach\(|\.map\(|\.filter\(|\.reduce\(" # JS iterators
26
+ r"|for\s+\w+\s+in\b" # python/ruby for x in
27
+ r"|for\s+\w+\s*:=" # go for x :=
28
+ r"|for\s+\w+\s+of\b" # JS for x of
29
+ )
30
+ RECURSION_RE = re.compile(
31
+ r"\b(def|func(?:tion)?|fn|sub)\s+(\w+)\s*\("
32
+ r"|public\s+(?:static\s+)?\w+\s+(\w+)\s*\("
33
+ )
34
+ CALL_RE = re.compile(r"\b(\w+)\s*\(")
35
+ SORT_RE = re.compile(r"\.sort\(|\.sort_by\(|\bsort\(|\bsorted\(|\bsort_by\b")
36
+ BINARY_RE = re.compile(r"\bbinary_search|bisect|\bmid\s*=\s*\(|while\s+\w+\s*<=\s*\w+.*(?:mid|lo|hi)")
37
+ HASH_RE = re.compile(r"\bdict\b|\bHashMap\b|\bMap\b|\bHashSet\b|\bSet\b|\b{}|\bHash\b")
38
+ NESTED_RE = re.compile(r"^\s+") # indentation marker
39
+
40
+
41
+ def _count_loops(code: str) -> tuple[int, int]:
42
+ """Returns (loop_count, max_nesting_depth) via indentation-aware scan."""
43
+ lines = code.splitlines()
44
+ indent_stack: list[int] = []
45
+ loops = 0
46
+ max_depth = 0
47
+ for raw in lines:
48
+ if not raw.strip() or raw.strip().startswith(("#", "//", "/*", "*")):
49
+ continue
50
+ indent = len(raw) - len(raw.lstrip())
51
+ while indent_stack and indent <= indent_stack[-1]:
52
+ indent_stack.pop()
53
+ if LOOP_RE.search(raw) and not raw.strip().startswith(("}", ")")):
54
+ loops += 1
55
+ indent_stack.append(indent)
56
+ max_depth = max(max_depth, len(indent_stack))
57
+ return loops, max_depth
58
+
59
+
60
+ def _detect_recursion(code: str) -> tuple[bool, str]:
61
+ """Best-effort recursion detection: does any function call itself?"""
62
+ funcs = []
63
+ for m in RECURSION_RE.finditer(code):
64
+ name = m.group(2) or m.group(3)
65
+ if name:
66
+ funcs.append(name)
67
+ for fn in funcs:
68
+ # find the function body and look for self-calls
69
+ m = re.search(rf"\b{fn}\s*\([^)]*\)[^#\n]*\n((?:.|\n)*?)(?=\n\S|\Z)", code)
70
+ if m and re.search(rf"\b{fn}\s*\(", m.group(1)):
71
+ return True, fn
72
+ return False, ""
73
+
74
+
75
+ def analyze(code: str, language: str = "python3") -> dict:
76
+ """Estimate asymptotic complexity from the code's STRUCTURE.
77
+
78
+ Loop counting and recursion detection come from a real parser
79
+ (`codecalc.parsing`). They used to come from regexes, which counted the word
80
+ "for" inside strings, comments and identifiers like `format()`, read a
81
+ chained `.map().filter().reduce()` as three loops, and called a docstring
82
+ mentioning a function's own name "recursion". tests/test_parsing_vs_regex.py
83
+ holds the divergent cases with the correct answer for each.
84
+
85
+ `analysis` in the result says which path produced the numbers, so a caller
86
+ can tell a parse from a fallback instead of assuming.
87
+ """
88
+ facts = parsing.analyse(code, language)
89
+ if facts.parsed:
90
+ loops, depth = facts.loops, facts.max_loop_depth
91
+ recursive = bool(facts.recursive_functions)
92
+ fn = facts.recursive_functions[0] if recursive else ""
93
+ analysis = "tree-sitter"
94
+ else:
95
+ # No grammar for this language, or the parser rejected the input. Say so
96
+ # rather than reporting a heuristic as though it were a parse.
97
+ loops, depth = _count_loops(code)
98
+ recursive, fn = _detect_recursion(code)
99
+ analysis = "regex-fallback"
100
+
101
+ # These two stay textual: "does this call a sort" and "does this use a hash
102
+ # map" are library questions, not grammar ones, and a parser has no more
103
+ # authority over them than a regex does.
104
+ has_sort = bool(SORT_RE.search(code))
105
+ has_hash = bool(HASH_RE.search(code))
106
+
107
+ # base estimate
108
+ if recursive:
109
+ estimate = "O(2^n) — recursion detected"
110
+ basis = f"recursive function '{fn}'"
111
+ elif depth >= 2:
112
+ estimate = f"O(n^{depth})"
113
+ basis = f"{depth} nested loop levels ({loops} loop constructs)"
114
+ elif loops == 1:
115
+ estimate = "O(n) — with sort: O(n log n)"
116
+ basis = "1 loop level"
117
+ elif has_sort and loops == 0:
118
+ estimate = "O(n log n)"
119
+ basis = "sorting call, no loops"
120
+ else:
121
+ estimate = "O(1)"
122
+ basis = "no loops, no recursion"
123
+
124
+ notes = []
125
+ if has_sort:
126
+ notes.append("sort/builtin detected (O(n log n) worst case)")
127
+ if has_hash:
128
+ notes.append("hash structures → average-case O(1) lookups")
129
+ if loops == 0 and not recursive and not has_sort:
130
+ notes.append("constant-time surface; may hide loops in library calls")
131
+
132
+ if analysis == "regex-fallback" and facts.reason:
133
+ notes.append(f"structural heuristic only ({facts.reason})")
134
+ if facts.parsed and facts.has_error:
135
+ notes.append("source has syntax errors; the parse is partial")
136
+
137
+ result = {
138
+ "ok": True,
139
+ "estimate": estimate,
140
+ "basis": basis,
141
+ "loops": loops,
142
+ "max_nesting": depth,
143
+ "recursion": recursive,
144
+ # TWO ORTHOGONAL FACTS, and only one of them was reported before.
145
+ # `analysis` says how the source was READ — parsed by tree-sitter, or
146
+ # guessed by regex when no grammar was available. `method` says whether
147
+ # the answer was MEASURED at all, and it never was: this counts loops
148
+ # and detects recursion. A caller could previously see
149
+ # analysis="tree-sitter" and reasonably hear "we determined the
150
+ # complexity", when the truth is "we read the structure accurately and
151
+ # inferred from it". `benchmark` is the empirical counterpart and
152
+ # reports method="empirical".
153
+ "method": "static-estimate",
154
+ "analysis": analysis,
155
+ "grammar": facts.grammar,
156
+ "functions": facts.functions if facts.parsed else [],
157
+ "notes": notes,
158
+ }
159
+
160
+ return result
161
+
162
+