sin-code-bundle 0.9.2__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.
- sin_code_bundle/__init__.py +6 -0
- sin_code_bundle/agents_md.py +245 -0
- sin_code_bundle/ast_edit.py +323 -0
- sin_code_bundle/bench.py +506 -0
- sin_code_bundle/budget.py +51 -0
- sin_code_bundle/cache.py +131 -0
- sin_code_bundle/checkpoint.py +230 -0
- sin_code_bundle/cli.py +1943 -0
- sin_code_bundle/codocs.py +328 -0
- sin_code_bundle/dap_bridge.py +135 -0
- sin_code_bundle/data/codocs/SKILL.md +280 -0
- sin_code_bundle/gitnexus.py +368 -0
- sin_code_bundle/hashline.py +216 -0
- sin_code_bundle/hooks.py +249 -0
- sin_code_bundle/immortal_commit.py +288 -0
- sin_code_bundle/interceptor.py +119 -0
- sin_code_bundle/lsp_backend.py +303 -0
- sin_code_bundle/lsp_bootstrap.py +85 -0
- sin_code_bundle/markitdown.py +254 -0
- sin_code_bundle/mcp_config.py +455 -0
- sin_code_bundle/mcp_server.py +963 -0
- sin_code_bundle/memory.py +208 -0
- sin_code_bundle/merge_safety.py +313 -0
- sin_code_bundle/orchestration_worktrees.py +102 -0
- sin_code_bundle/policy.py +224 -0
- sin_code_bundle/preflight.py +152 -0
- sin_code_bundle/programming_workflow.py +541 -0
- sin_code_bundle/rtk.py +154 -0
- sin_code_bundle/safety.py +52 -0
- sin_code_bundle/session_warmup.py +247 -0
- sin_code_bundle/skills.py +188 -0
- sin_code_bundle/symbol_resolve.py +166 -0
- sin_code_bundle/tools/__init__.py +4 -0
- sin_code_bundle/tools/pypi_setup.py +289 -0
- sin_code_bundle/vfs.py +264 -0
- sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
- sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
- sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
- sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
- sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
- sin_code_bundle-0.9.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Purpose: Pre-refactor checkpoint — snapshot + state report in 1 call.
|
|
2
|
+
# Docs: checkpoint.doc.md
|
|
3
|
+
"""Consolidates rollback_snapshot + codocs_check + git status + sin_search
|
|
4
|
+
+ pytest collection. Idempotent — safe to call twice with the same name
|
|
5
|
+
(returns existing snapshot id).
|
|
6
|
+
|
|
7
|
+
Docs: checkpoint.doc.md
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
# Hard-coded fallback for the dev-machine layout from AGENTS.md so the
|
|
19
|
+
# MCP stdio process (which may have a stripped PATH) can still find the
|
|
20
|
+
# rollback CLI.
|
|
21
|
+
_ROLLBACK_FALLBACK = "/Users/jeremy/Library/Python/3.14/bin/sin-honcho-rollback"
|
|
22
|
+
_SCOUT_FALLBACK = "/Users/jeremy/.local/bin/scout"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Checkpointer:
|
|
26
|
+
"""Pre-refactor checkpoint orchestrator.
|
|
27
|
+
|
|
28
|
+
Creates a recoverable state AND reports on the current state of the
|
|
29
|
+
working tree. Idempotent on ``name`` — calling twice with the same name
|
|
30
|
+
does not create a duplicate snapshot.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
repo_root: Optional[Path] = None,
|
|
36
|
+
db_path: str = ".sin/rollback.db",
|
|
37
|
+
) -> None:
|
|
38
|
+
self.repo_root = Path(repo_root) if repo_root else Path.cwd()
|
|
39
|
+
self.db_path = db_path
|
|
40
|
+
|
|
41
|
+
def create(
|
|
42
|
+
self,
|
|
43
|
+
name: str,
|
|
44
|
+
include: Optional[List[str]] = None,
|
|
45
|
+
description: str = "",
|
|
46
|
+
) -> Dict[str, Any]:
|
|
47
|
+
"""Create checkpoint. Idempotent on ``name``.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
name: snapshot name (e.g. ``"before-auth-refactor"``).
|
|
51
|
+
include: subset of {snapshot, docs, git, usages, tests}.
|
|
52
|
+
Defaults to all five.
|
|
53
|
+
description: optional human-readable description.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dict with ``snapshot_id``, per-check counts, and per-check
|
|
57
|
+
error fields when something fails. Always returns (never
|
|
58
|
+
raises) so the caller can safely merge the result into a
|
|
59
|
+
larger state report.
|
|
60
|
+
"""
|
|
61
|
+
if include is None:
|
|
62
|
+
include = ["snapshot", "docs", "git", "usages", "tests"]
|
|
63
|
+
|
|
64
|
+
result: Dict[str, Any] = {
|
|
65
|
+
"checkpoint_name": name,
|
|
66
|
+
"include": include,
|
|
67
|
+
"snapshot_id": None,
|
|
68
|
+
"docs_broken": 0,
|
|
69
|
+
"git_clean": True,
|
|
70
|
+
"git_changes_count": 0,
|
|
71
|
+
"usages_found": 0,
|
|
72
|
+
"tests_status": "unknown",
|
|
73
|
+
"tests_collected": None,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ── 1. Snapshot (sin-honcho-rollback) ───────────────────────────
|
|
77
|
+
# Skipped silently when the CLI is not installed (e.g. minimal
|
|
78
|
+
# install) — the state report is still useful without a snapshot.
|
|
79
|
+
if "snapshot" in include:
|
|
80
|
+
try:
|
|
81
|
+
rb_bin = shutil.which("sin-honcho-rollback") or _ROLLBACK_FALLBACK
|
|
82
|
+
if Path(rb_bin).exists():
|
|
83
|
+
proc = subprocess.run(
|
|
84
|
+
[
|
|
85
|
+
rb_bin,
|
|
86
|
+
"snapshot",
|
|
87
|
+
name,
|
|
88
|
+
"--description",
|
|
89
|
+
description or f"Pre-change checkpoint: {name}",
|
|
90
|
+
"--db",
|
|
91
|
+
str(self.repo_root / self.db_path),
|
|
92
|
+
],
|
|
93
|
+
capture_output=True,
|
|
94
|
+
text=True,
|
|
95
|
+
timeout=10,
|
|
96
|
+
)
|
|
97
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
98
|
+
data = json.loads(proc.stdout)
|
|
99
|
+
# The CLI nests the id under "snapshot.id" — fall
|
|
100
|
+
# back to top-level "id" for older schemas.
|
|
101
|
+
result["snapshot_id"] = data.get("snapshot", {}).get("id") or data.get("id")
|
|
102
|
+
else:
|
|
103
|
+
result["snapshot_error"] = proc.stderr[-500:]
|
|
104
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception) as exc:
|
|
105
|
+
result["snapshot_error"] = str(exc)
|
|
106
|
+
|
|
107
|
+
# ── 2. Docs (codocs.find_broken) ────────────────────────────────
|
|
108
|
+
if "docs" in include:
|
|
109
|
+
try:
|
|
110
|
+
from . import codocs
|
|
111
|
+
|
|
112
|
+
broken = codocs.find_broken(str(self.repo_root))
|
|
113
|
+
result["docs_broken"] = len(broken)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
result["docs_error"] = str(exc)
|
|
116
|
+
|
|
117
|
+
# ── 3. Git status ───────────────────────────────────────────────
|
|
118
|
+
if "git" in include:
|
|
119
|
+
try:
|
|
120
|
+
if (self.repo_root / ".git").exists():
|
|
121
|
+
proc = subprocess.run(
|
|
122
|
+
["git", "status", "--porcelain"],
|
|
123
|
+
cwd=self.repo_root,
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
timeout=5,
|
|
127
|
+
)
|
|
128
|
+
if proc.returncode == 0:
|
|
129
|
+
changes = proc.stdout.strip()
|
|
130
|
+
result["git_clean"] = not bool(changes)
|
|
131
|
+
if changes:
|
|
132
|
+
result["git_changes_count"] = len(changes.split("\n"))
|
|
133
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as exc:
|
|
134
|
+
result["git_error"] = str(exc)
|
|
135
|
+
|
|
136
|
+
# ── 4. Usages (scout, with grep fallback) ───────────────────────
|
|
137
|
+
# scout gives better ranking + cross-source context; grep is the
|
|
138
|
+
# always-available fallback that still answers "where is X used?".
|
|
139
|
+
if "usages" in include:
|
|
140
|
+
result["usages_found"] = self._count_usages(name)
|
|
141
|
+
|
|
142
|
+
# ── 5. Tests (pytest --collect-only) ────────────────────────────
|
|
143
|
+
if "tests" in include:
|
|
144
|
+
try:
|
|
145
|
+
has_tests = (self.repo_root / "tests").exists() or (
|
|
146
|
+
self.repo_root / "test"
|
|
147
|
+
).exists()
|
|
148
|
+
if has_tests:
|
|
149
|
+
proc = subprocess.run(
|
|
150
|
+
["python3", "-m", "pytest", "--collect-only", "-q"],
|
|
151
|
+
cwd=self.repo_root,
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
timeout=15,
|
|
155
|
+
)
|
|
156
|
+
if proc.returncode == 0:
|
|
157
|
+
result["tests_status"] = "pass"
|
|
158
|
+
for line in proc.stdout.split("\n"):
|
|
159
|
+
if "tests collected" in line.lower():
|
|
160
|
+
result["tests_collected"] = line.strip()
|
|
161
|
+
break
|
|
162
|
+
else:
|
|
163
|
+
result["tests_status"] = "fail"
|
|
164
|
+
except subprocess.TimeoutExpired:
|
|
165
|
+
result["tests_status"] = "timeout"
|
|
166
|
+
except FileNotFoundError:
|
|
167
|
+
result["tests_status"] = "skipped"
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
result["tests_error"] = str(exc)
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def _count_usages(self, name: str) -> int:
|
|
174
|
+
"""Best-effort usage count for ``name``.
|
|
175
|
+
|
|
176
|
+
Tries ``scout --type usage`` first (better ranking), then falls back
|
|
177
|
+
to ``grep -r -l -w`` for the always-available case. Returns 0 when
|
|
178
|
+
neither tool is available so the caller can still reason about the
|
|
179
|
+
result.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
scout_bin = shutil.which("scout") or _SCOUT_FALLBACK
|
|
183
|
+
if Path(scout_bin).exists():
|
|
184
|
+
proc = subprocess.run(
|
|
185
|
+
[
|
|
186
|
+
scout_bin,
|
|
187
|
+
"--query",
|
|
188
|
+
name,
|
|
189
|
+
"--type",
|
|
190
|
+
"usage",
|
|
191
|
+
"--path",
|
|
192
|
+
str(self.repo_root),
|
|
193
|
+
"--format",
|
|
194
|
+
"json",
|
|
195
|
+
],
|
|
196
|
+
capture_output=True,
|
|
197
|
+
text=True,
|
|
198
|
+
timeout=10,
|
|
199
|
+
)
|
|
200
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
201
|
+
data = json.loads(proc.stdout)
|
|
202
|
+
return len(data.get("results", []))
|
|
203
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
# Fallback: grep -r -l -w (counts files, not occurrences).
|
|
207
|
+
try:
|
|
208
|
+
proc = subprocess.run(
|
|
209
|
+
[
|
|
210
|
+
"grep",
|
|
211
|
+
"-r",
|
|
212
|
+
"-l",
|
|
213
|
+
"-w",
|
|
214
|
+
name,
|
|
215
|
+
str(self.repo_root),
|
|
216
|
+
"--include=*.py",
|
|
217
|
+
"--include=*.ts",
|
|
218
|
+
"--include=*.js",
|
|
219
|
+
"--include=*.go",
|
|
220
|
+
],
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
timeout=10,
|
|
224
|
+
)
|
|
225
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
226
|
+
return len(proc.stdout.strip().split("\n"))
|
|
227
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
return 0
|