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 +9 -0
- tycho/__main__.py +12 -0
- tycho/astdiff.py +102 -0
- tycho/checks.py +681 -0
- tycho/cli.py +516 -0
- tycho/config.py +177 -0
- tycho/doctor.py +304 -0
- tycho/events.py +393 -0
- tycho/fsstate.py +30 -0
- tycho/gitstate.py +46 -0
- tycho/harness.py +303 -0
- tycho/hook.py +212 -0
- tycho/init.py +1018 -0
- tycho/model.py +147 -0
- tycho/opencode.py +154 -0
- tycho/report.py +29 -0
- tycho/runlog.py +54 -0
- tycho/state.py +469 -0
- tycho/status.py +171 -0
- tycho/verify.py +202 -0
- tycho/version.py +99 -0
- tycho_cli-0.0.1.dist-info/METADATA +379 -0
- tycho_cli-0.0.1.dist-info/RECORD +26 -0
- tycho_cli-0.0.1.dist-info/WHEEL +4 -0
- tycho_cli-0.0.1.dist-info/entry_points.txt +2 -0
- tycho_cli-0.0.1.dist-info/licenses/LICENSE +201 -0
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
|