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.
Files changed (41) hide show
  1. sin_code_bundle/__init__.py +6 -0
  2. sin_code_bundle/agents_md.py +245 -0
  3. sin_code_bundle/ast_edit.py +323 -0
  4. sin_code_bundle/bench.py +506 -0
  5. sin_code_bundle/budget.py +51 -0
  6. sin_code_bundle/cache.py +131 -0
  7. sin_code_bundle/checkpoint.py +230 -0
  8. sin_code_bundle/cli.py +1943 -0
  9. sin_code_bundle/codocs.py +328 -0
  10. sin_code_bundle/dap_bridge.py +135 -0
  11. sin_code_bundle/data/codocs/SKILL.md +280 -0
  12. sin_code_bundle/gitnexus.py +368 -0
  13. sin_code_bundle/hashline.py +216 -0
  14. sin_code_bundle/hooks.py +249 -0
  15. sin_code_bundle/immortal_commit.py +288 -0
  16. sin_code_bundle/interceptor.py +119 -0
  17. sin_code_bundle/lsp_backend.py +303 -0
  18. sin_code_bundle/lsp_bootstrap.py +85 -0
  19. sin_code_bundle/markitdown.py +254 -0
  20. sin_code_bundle/mcp_config.py +455 -0
  21. sin_code_bundle/mcp_server.py +963 -0
  22. sin_code_bundle/memory.py +208 -0
  23. sin_code_bundle/merge_safety.py +313 -0
  24. sin_code_bundle/orchestration_worktrees.py +102 -0
  25. sin_code_bundle/policy.py +224 -0
  26. sin_code_bundle/preflight.py +152 -0
  27. sin_code_bundle/programming_workflow.py +541 -0
  28. sin_code_bundle/rtk.py +154 -0
  29. sin_code_bundle/safety.py +52 -0
  30. sin_code_bundle/session_warmup.py +247 -0
  31. sin_code_bundle/skills.py +188 -0
  32. sin_code_bundle/symbol_resolve.py +166 -0
  33. sin_code_bundle/tools/__init__.py +4 -0
  34. sin_code_bundle/tools/pypi_setup.py +289 -0
  35. sin_code_bundle/vfs.py +264 -0
  36. sin_code_bundle-0.9.2.dist-info/METADATA +470 -0
  37. sin_code_bundle-0.9.2.dist-info/RECORD +41 -0
  38. sin_code_bundle-0.9.2.dist-info/WHEEL +5 -0
  39. sin_code_bundle-0.9.2.dist-info/entry_points.txt +4 -0
  40. sin_code_bundle-0.9.2.dist-info/licenses/LICENSE +21 -0
  41. 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