tycho-cli 0.0.1__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.
tycho/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Tycho — a local verifier that fires when an agent claims it's done.
2
+
3
+ Proves the agent's work from what lives on the developer's machine: git, the filesystem,
4
+ process exit codes, and the harness event stream. Only code renders a verdict — no LLM in the
5
+ trust path. Free, open source (Apache 2.0), offline, no account needed.
6
+ """
7
+
8
+ # Alpha. Bumped to 0.1.0 for the first public release (TYCHO-10).
9
+ __version__ = "0.0.1"
tycho/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Run Tycho as a module: `python -m tycho`.
2
+
3
+ Mirrors the `tycho` console script (`tycho.cli:main`). Also the freeze entry point for
4
+ standalone binaries (TYCHO-69) — PyInstaller/zipapp target the package, no wrapper script.
5
+ """
6
+
7
+ import sys
8
+
9
+ from tycho.cli import main
10
+
11
+ if __name__ == "__main__":
12
+ sys.exit(main())
tycho/astdiff.py ADDED
@@ -0,0 +1,102 @@
1
+ """Compare two versions of a Python source file for tell-tale test-gaming edits.
2
+
3
+ Stdlib `ast` only. Every entry point returns evidence strings (empty = nothing
4
+ found), and returns `[]` when either side won't parse — an unparseable file is
5
+ UNSUPPORTED, never a false FAIL.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+
12
+ _MOCK_NAMES = frozenset({"patch", "mock", "Mock", "MagicMock", "AsyncMock"})
13
+
14
+
15
+ def assertion_delta(before: str | None, after: str | None) -> list[str]:
16
+ """Assertions removed or neutralized to always-true between before and after."""
17
+ b, a = _parse(before), _parse(after)
18
+ if b is None or a is None:
19
+ return []
20
+ findings = []
21
+ removed = _count_asserts(b) - _count_asserts(a)
22
+ if removed > 0:
23
+ findings.append(f"{removed} assertion(s) removed")
24
+ neutralized = _count_neutralized(a) - _count_neutralized(b)
25
+ if neutralized > 0:
26
+ findings.append(f"{neutralized} assertion(s) neutralized to always-true")
27
+ return findings
28
+
29
+
30
+ def skip_or_mock_added(before: str | None, after: str | None) -> list[str]:
31
+ """Skip decorators or mock/patch uses newly introduced in after."""
32
+ b, a = _parse(before), _parse(after)
33
+ if b is None or a is None:
34
+ return []
35
+ findings = []
36
+ skips_before, skips_after = _skip_decorators(b), _skip_decorators(a)
37
+ if len(skips_after) > len(skips_before):
38
+ added = sorted(set(skips_after) - set(skips_before)) or skips_after
39
+ findings.append(f"skip added: {', '.join(added)}")
40
+ mocks_added = _mock_uses(a) - _mock_uses(b)
41
+ if mocks_added > 0:
42
+ findings.append(f"{mocks_added} mock/patch use(s) added")
43
+ return findings
44
+
45
+
46
+ def _parse(src: str | None) -> ast.AST | None:
47
+ if not src:
48
+ return None
49
+ try:
50
+ return ast.parse(src)
51
+ except (SyntaxError, ValueError):
52
+ return None
53
+
54
+
55
+ def _dotted(node: ast.AST) -> str:
56
+ if isinstance(node, ast.Name):
57
+ return node.id
58
+ if isinstance(node, ast.Attribute):
59
+ return f"{_dotted(node.value)}.{node.attr}"
60
+ if isinstance(node, ast.Call):
61
+ return _dotted(node.func)
62
+ return ""
63
+
64
+
65
+ def _count_asserts(tree: ast.AST) -> int:
66
+ """`assert` statements plus unittest-style `self.assert*(...)` calls."""
67
+ count = 0
68
+ for node in ast.walk(tree):
69
+ if isinstance(node, ast.Assert):
70
+ count += 1
71
+ elif isinstance(node, ast.Call) and _dotted(node.func).rsplit(".", 1)[-1].startswith("assert"):
72
+ count += 1
73
+ return count
74
+
75
+
76
+ def _count_neutralized(tree: ast.AST) -> int:
77
+ """`assert True` / `assert 1` — asserts that can never fail."""
78
+ return sum(
79
+ 1
80
+ for node in ast.walk(tree)
81
+ if isinstance(node, ast.Assert)
82
+ and isinstance(node.test, ast.Constant)
83
+ and bool(node.test.value)
84
+ )
85
+
86
+
87
+ def _skip_decorators(tree: ast.AST) -> list[str]:
88
+ out = []
89
+ for node in ast.walk(tree):
90
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
91
+ out.extend(name for d in node.decorator_list if "skip" in (name := _dotted(d)).lower())
92
+ return out
93
+
94
+
95
+ def _mock_uses(tree: ast.AST) -> int:
96
+ count = 0
97
+ for node in ast.walk(tree):
98
+ if isinstance(node, ast.Name) and node.id in _MOCK_NAMES:
99
+ count += 1
100
+ elif isinstance(node, ast.Attribute) and node.attr in _MOCK_NAMES:
101
+ count += 1
102
+ return count