sourcepack 1.10.0a0__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.
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import time
11
+ import uuid
12
+ from dataclasses import asdict, dataclass
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Iterable
16
+
17
+ from sourcepack import __version__
18
+
19
+ SCHEMA_VERSION = "sourcepack.execution_ledger.v1"
20
+ LEDGER_FILENAME = "ledger.jsonl"
21
+ MAX_EXCERPT_CHARS = 2048
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ExecutionClaim:
26
+ command: str
27
+ phrase: str
28
+ start: int
29
+ end: int
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ExecutionLedgerEntry:
34
+ schema_version: str
35
+ entry_id: str
36
+ generated_at: str
37
+ repo_root: str
38
+ git_head: str | None
39
+ worktree_dirty_before: bool | None
40
+ worktree_dirty_after: bool | None
41
+ command: list[str]
42
+ cwd: str
43
+ exit_code: int
44
+ stdout_sha256: str
45
+ stderr_sha256: str
46
+ stdout_excerpt: str
47
+ stderr_excerpt: str
48
+ duration_ms: int
49
+ environment_summary: dict
50
+ sourcepack_version: str
51
+
52
+
53
+ def utc_now() -> str:
54
+ return datetime.now(timezone.utc).isoformat()
55
+
56
+
57
+ def sha256_bytes(data: bytes) -> str:
58
+ return hashlib.sha256(data).hexdigest()
59
+
60
+
61
+ def excerpt_bytes(data: bytes, limit: int = MAX_EXCERPT_CHARS) -> str:
62
+ text = data.decode("utf-8", "replace")
63
+ if len(text) <= limit:
64
+ return text
65
+ return text[:limit] + "…[truncated]"
66
+
67
+
68
+ def find_repo_root(start: str | Path = ".") -> Path:
69
+ start_path = Path(start).resolve()
70
+ cp = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=start_path, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
71
+ if cp.returncode == 0 and cp.stdout.strip():
72
+ return Path(cp.stdout.strip()).resolve()
73
+ return start_path
74
+
75
+
76
+ def _git_head(repo_root: Path) -> str | None:
77
+ cp = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo_root, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
78
+ return cp.stdout.strip() if cp.returncode == 0 and cp.stdout.strip() else None
79
+
80
+
81
+ def _worktree_dirty(repo_root: Path) -> bool | None:
82
+ cp = subprocess.run(["git", "status", "--porcelain"], cwd=repo_root, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
83
+ if cp.returncode != 0:
84
+ return None
85
+ return bool(cp.stdout.strip())
86
+
87
+
88
+ def ledger_dir(repo_root: str | Path) -> Path:
89
+ return Path(repo_root) / ".sourcepack" / "evidence"
90
+
91
+
92
+ def ledger_path(repo_root: str | Path) -> Path:
93
+ return ledger_dir(repo_root) / LEDGER_FILENAME
94
+
95
+
96
+ def entry_to_json(entry: ExecutionLedgerEntry) -> str:
97
+ return json.dumps(asdict(entry), sort_keys=True, separators=(",", ":"))
98
+
99
+
100
+ def append_entry(repo_root: str | Path, entry: ExecutionLedgerEntry) -> None:
101
+ path = ledger_path(repo_root)
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ with path.open("a", encoding="utf-8") as fh:
104
+ fh.write(entry_to_json(entry) + "\n")
105
+
106
+
107
+ def iter_entries(repo_root: str | Path) -> Iterable[dict]:
108
+ path = ledger_path(repo_root)
109
+ if not path.exists():
110
+ return
111
+ with path.open("r", encoding="utf-8") as fh:
112
+ for line in fh:
113
+ line = line.strip()
114
+ if line:
115
+ yield json.loads(line)
116
+
117
+
118
+ def clear_ledger(repo_root: str | Path) -> None:
119
+ path = ledger_path(repo_root)
120
+ if path.exists():
121
+ path.unlink()
122
+
123
+
124
+ def environment_summary() -> dict:
125
+ return {
126
+ "platform": platform.platform(),
127
+ "python": platform.python_version(),
128
+ "shell": os.environ.get("SHELL"),
129
+ "path_entries": len(os.environ.get("PATH", "").split(os.pathsep)) if os.environ.get("PATH") else 0,
130
+ }
131
+
132
+
133
+ def run_and_record(command: list[str], cwd: str | Path = ".") -> ExecutionLedgerEntry:
134
+ if not command:
135
+ raise ValueError("sourcepack exec requires a command after --")
136
+ repo_root = find_repo_root(cwd)
137
+ dirty_before = _worktree_dirty(repo_root)
138
+ head = _git_head(repo_root)
139
+ start = time.monotonic()
140
+ cp = subprocess.run(command, cwd=Path(cwd).resolve(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
141
+ duration_ms = int((time.monotonic() - start) * 1000)
142
+ dirty_after = _worktree_dirty(repo_root)
143
+ entry = ExecutionLedgerEntry(
144
+ schema_version=SCHEMA_VERSION,
145
+ entry_id=uuid.uuid4().hex,
146
+ generated_at=utc_now(),
147
+ repo_root=str(repo_root),
148
+ git_head=head,
149
+ worktree_dirty_before=dirty_before,
150
+ worktree_dirty_after=dirty_after,
151
+ command=list(command),
152
+ cwd=str(Path(cwd).resolve()),
153
+ exit_code=int(cp.returncode),
154
+ stdout_sha256=sha256_bytes(cp.stdout),
155
+ stderr_sha256=sha256_bytes(cp.stderr),
156
+ stdout_excerpt=excerpt_bytes(cp.stdout),
157
+ stderr_excerpt=excerpt_bytes(cp.stderr),
158
+ duration_ms=duration_ms,
159
+ environment_summary=environment_summary(),
160
+ sourcepack_version=__version__,
161
+ )
162
+ append_entry(repo_root, entry)
163
+ return entry
164
+
165
+
166
+ _CLEAR_PHRASES = [
167
+ "tests passed", "test passed", "build passed", "lint passed", "typecheck passed",
168
+ "pytest passed", "npm test passed", "npm run build passed",
169
+ ]
170
+ _SUPPORTED_COMMAND_PREFIXES = ["pytest", "npm test", "npm run build", "npm run test", "python -m pytest", "make test", "ruff check", "mypy"]
171
+ _RAN_RE = re.compile(r"\bI\s+(?:ran|tested)\s+([^\n.;]+)", re.IGNORECASE)
172
+
173
+
174
+ def detect_execution_claims(text: str) -> list[ExecutionClaim]:
175
+ """Return bounded, explicit command-execution claims without semantic guessing."""
176
+ claims: list[ExecutionClaim] = []
177
+ lower = text.lower()
178
+ for phrase in _CLEAR_PHRASES:
179
+ start = lower.find(phrase)
180
+ while start != -1:
181
+ if not re.search(r"\b(should|probably|expected to)\s+" + re.escape(phrase.split()[0]), lower[max(0, start-20):start+len(phrase)]):
182
+ cmd = phrase.removesuffix(" passed")
183
+ claims.append(ExecutionClaim(command=cmd, phrase=text[start:start + len(phrase)], start=start, end=start + len(phrase)))
184
+ start = lower.find(phrase, start + 1)
185
+ for prefix in _SUPPORTED_COMMAND_PREFIXES:
186
+ pattern = re.compile(r"\b" + re.escape(prefix) + r"\s+(passed|works|succeeds)\b", re.IGNORECASE)
187
+ for m in pattern.finditer(text):
188
+ claims.append(ExecutionClaim(command=prefix, phrase=m.group(0), start=m.start(), end=m.end()))
189
+ for m in _RAN_RE.finditer(text):
190
+ cmd = m.group(1).strip().strip('`"\'')
191
+ if cmd and len(cmd.split()) <= 8 and not cmd.lower().startswith(("tests", "the test file")):
192
+ claims.append(ExecutionClaim(command=cmd, phrase=m.group(0), start=m.start(), end=m.end()))
193
+ claims.sort(key=lambda c: (c.start, c.end, c.command))
194
+ deduped: list[ExecutionClaim] = []
195
+ seen = set()
196
+ for claim in claims:
197
+ key = (claim.command.lower(), claim.start, claim.end)
198
+ if key not in seen:
199
+ seen.add(key)
200
+ deduped.append(claim)
201
+ return deduped
202
+
203
+
204
+ def _command_matches(claim: str, entry_command: list[str]) -> bool:
205
+ normalized_entry = " ".join(entry_command).strip().lower()
206
+ normalized_claim = claim.strip().lower()
207
+ return normalized_entry == normalized_claim or normalized_entry.startswith(normalized_claim + " ")
208
+
209
+
210
+ def evidence_for_claim(repo_root: str | Path, claim: ExecutionClaim) -> tuple[str, dict | None]:
211
+ matches = [entry for entry in iter_entries(repo_root) if _command_matches(claim.command, list(entry.get("command") or []))]
212
+ if not matches:
213
+ return "execution_evidence_missing", None
214
+ latest = sorted(matches, key=lambda e: str(e.get("generated_at") or ""))[-1]
215
+ if len({int(m.get("exit_code", -999)) for m in matches}) > 1:
216
+ return "execution_inconclusive", latest
217
+ if int(latest.get("exit_code", -1)) == 0:
218
+ return "execution_evidence_present", latest
219
+ return "execution_failed", latest
220
+
221
+
222
+ def execution_findings(repo_root: str | Path, text: str) -> list[dict]:
223
+ findings: list[dict] = []
224
+ for claim in detect_execution_claims(text):
225
+ status, entry = evidence_for_claim(repo_root, claim)
226
+ if status == "execution_evidence_present":
227
+ severity = "info"
228
+ message = f"Execution ledger contains a successful local run for: {claim.command}."
229
+ elif status == "execution_failed":
230
+ severity = "warn"
231
+ message = f"Execution ledger contains a failed local run for: {claim.command}."
232
+ elif status == "execution_inconclusive":
233
+ severity = "warn"
234
+ message = f"Execution ledger has mixed or ambiguous local runs for: {claim.command}."
235
+ else:
236
+ severity = "warn"
237
+ message = f"No SourcePack execution-ledger entry supports claimed run: {claim.command}."
238
+ findings.append({
239
+ "id": status,
240
+ "severity": severity,
241
+ "category": "execution",
242
+ "path": None,
243
+ "message": message,
244
+ "evidence": claim.command,
245
+ "suggestion": "Run the command through `sourcepack exec -- ...` if local execution evidence is intended." if severity == "warn" else None,
246
+ "ledger_entry_id": entry.get("entry_id") if entry else None,
247
+ })
248
+ return findings
249
+
250
+
251
+ def command_available(command: str) -> bool:
252
+ return shutil.which(command) is not None
sourcepack/git.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+
7
+ def run_git(repo: str | Path, args: list[str]) -> subprocess.CompletedProcess:
8
+ try:
9
+ return subprocess.run(["git", *args], cwd=Path(repo), text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
10
+ except FileNotFoundError:
11
+ return subprocess.CompletedProcess(["git", *args], 127, "", "git executable not found")
12
+
13
+
14
+ def repo_root(path: str | Path) -> Path | None:
15
+ cp = run_git(path, ["rev-parse", "--show-toplevel"])
16
+ return Path(cp.stdout.strip()).resolve() if cp.returncode == 0 else None
17
+
18
+
19
+ def diff(repo: str | Path, *, staged: bool = False, relative: bool = False) -> str:
20
+ args = ["diff", "--staged"] if staged else ["diff"]
21
+ if relative:
22
+ args.append("--relative")
23
+ return run_git(repo, args).stdout
24
+
25
+
26
+ def untracked_files(repo: str | Path) -> list[str]:
27
+ cp = run_git(repo, ["ls-files", "--others", "--exclude-standard"])
28
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()] if cp.returncode == 0 else []
29
+
30
+
31
+ def dirty_worktree(repo: str | Path) -> tuple[bool, str | None]:
32
+ root = repo_root(repo)
33
+ if root is None:
34
+ cp = run_git(repo, ["rev-parse", "--show-toplevel"])
35
+ return False, "git_unavailable" if cp.returncode == 127 else "not_git"
36
+ for args in (["diff", "--quiet"], ["diff", "--staged", "--quiet"]):
37
+ cp = run_git(root, list(args))
38
+ if cp.returncode == 1:
39
+ return True, None
40
+ if cp.returncode == 127:
41
+ return False, "git_unavailable"
42
+ return (bool(untracked_files(root)), None)
43
+
44
+
45
+ def metadata(repo: str | Path) -> dict:
46
+ root = Path(repo)
47
+ head = run_git(root, ["rev-parse", "HEAD"])
48
+ branch = run_git(root, ["rev-parse", "--abbrev-ref", "HEAD"])
49
+ dirty, state = dirty_worktree(root)
50
+ return {"branch": branch.stdout.strip() if branch.returncode == 0 else None, "head_commit": head.stdout.strip() if head.returncode == 0 else None, "dirty": dirty if state is None else None, "dirty_state": state}