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.
- sourcepack/__init__.py +19 -0
- sourcepack/assets/__init__.py +1 -0
- sourcepack/assets/audit_template.md +3 -0
- sourcepack/assets/packet_instructions.md +3 -0
- sourcepack/baseline.py +285 -0
- sourcepack/cli.py +2991 -0
- sourcepack/commands.py +149 -0
- sourcepack/dependencies.py +98 -0
- sourcepack/diff_parser.py +122 -0
- sourcepack/ecosystems/__init__.py +3 -0
- sourcepack/ecosystems/generic.py +13 -0
- sourcepack/ecosystems/node.py +3 -0
- sourcepack/ecosystems/python.py +12 -0
- sourcepack/errors.py +19 -0
- sourcepack/evidence.py +109 -0
- sourcepack/execution_ledger.py +252 -0
- sourcepack/git.py +50 -0
- sourcepack/judgment.py +1922 -0
- sourcepack/packet.py +837 -0
- sourcepack/paths.py +68 -0
- sourcepack/policy.py +38 -0
- sourcepack/reason_codes.py +72 -0
- sourcepack/reports/__init__.py +5 -0
- sourcepack/reports/html.py +88 -0
- sourcepack/reports/json.py +123 -0
- sourcepack/reports/markdown.py +61 -0
- sourcepack/schemas.py +63 -0
- sourcepack-1.10.0a0.dist-info/METADATA +311 -0
- sourcepack-1.10.0a0.dist-info/RECORD +33 -0
- sourcepack-1.10.0a0.dist-info/WHEEL +5 -0
- sourcepack-1.10.0a0.dist-info/entry_points.txt +2 -0
- sourcepack-1.10.0a0.dist-info/licenses/LICENSE +21 -0
- sourcepack-1.10.0a0.dist-info/top_level.txt +1 -0
sourcepack/commands.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
COMMAND_SCHEMA_VERSION = "sourcepack.command_resolver.v1"
|
|
10
|
+
COMPOSE_FILES = ("compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CommandResolution:
|
|
15
|
+
verdict: str
|
|
16
|
+
reason_code: str | None
|
|
17
|
+
command: str
|
|
18
|
+
evidence_source: str | None = None
|
|
19
|
+
message: str = ""
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict:
|
|
22
|
+
return {"schema_version": COMMAND_SCHEMA_VERSION, "verdict": self.verdict, "reason_code": self.reason_code, "command": self.command, "evidence_source": self.evidence_source, "message": self.message}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe(root: Path, rel: str) -> Path | None:
|
|
26
|
+
p = (root / rel).resolve()
|
|
27
|
+
try:
|
|
28
|
+
p.relative_to(root.resolve())
|
|
29
|
+
except ValueError:
|
|
30
|
+
return None
|
|
31
|
+
return p
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _read_json(path: Path) -> dict | None:
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _make_targets(text: str) -> set[str]:
|
|
42
|
+
return {m.group(1) for m in re.finditer(r"^([A-Za-z0-9_.-][^\s:=]*)\s*:(?!=)", text, re.M)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _just_targets(text: str) -> set[str]:
|
|
46
|
+
return {m.group(1) for m in re.finditer(r"^([A-Za-z0-9_.-]+)\s*:", text, re.M)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _taskfile_targets(data: dict) -> set[str]:
|
|
50
|
+
tasks = data.get("tasks") if isinstance(data, dict) else None
|
|
51
|
+
return set(tasks.keys()) if isinstance(tasks, dict) else set()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_command(root: str | Path, command: str, *, added_manifests: dict[str, str] | None = None) -> CommandResolution:
|
|
55
|
+
root = Path(root).resolve(); added_manifests = added_manifests or {}; command = command.strip()
|
|
56
|
+
parts = command.split()
|
|
57
|
+
if not parts:
|
|
58
|
+
return CommandResolution("WARN", "command_check_inconclusive", command, message="empty command")
|
|
59
|
+
if len(parts) >= 3 and parts[0] == "npm" and parts[1] == "run":
|
|
60
|
+
script = parts[2]
|
|
61
|
+
pj = _safe(root, "package.json")
|
|
62
|
+
if "package.json" in added_manifests:
|
|
63
|
+
data = _read_json_from_text(added_manifests["package.json"])
|
|
64
|
+
if script in (data.get("scripts") or {}):
|
|
65
|
+
return CommandResolution("WARN", "declared_command", command, "package.json", "script added in patch")
|
|
66
|
+
if not pj or not pj.exists():
|
|
67
|
+
return CommandResolution("WARN", "command_manifest_missing", command, "package.json", "package.json missing")
|
|
68
|
+
data = _read_json(pj) or {}
|
|
69
|
+
return CommandResolution("PASS", None, command, "package.json", "script present") if script in (data.get("scripts") or {}) else CommandResolution("FAIL", "unsupported_command", command, "package.json", "npm script missing")
|
|
70
|
+
if len(parts) >= 3 and parts[0] == "docker" and parts[1] == "compose":
|
|
71
|
+
for name in COMPOSE_FILES:
|
|
72
|
+
p = _safe(root, name)
|
|
73
|
+
if p and p.exists():
|
|
74
|
+
return CommandResolution("PASS", None, command, name, "compose file present")
|
|
75
|
+
return CommandResolution("FAIL", "unsupported_command", command, ",".join(COMPOSE_FILES), "compose file missing")
|
|
76
|
+
if parts[0] == "make" and len(parts) >= 2:
|
|
77
|
+
p = _safe(root, "Makefile")
|
|
78
|
+
if not p or not p.exists():
|
|
79
|
+
return CommandResolution("WARN", "command_manifest_missing", command, "Makefile", "Makefile missing")
|
|
80
|
+
targets = _make_targets(p.read_text(encoding="utf-8", errors="ignore"))
|
|
81
|
+
return CommandResolution("PASS", None, command, "Makefile", "target present") if parts[1] in targets else CommandResolution("FAIL", "unsupported_command", command, "Makefile", "Make target missing")
|
|
82
|
+
if parts[0] == "just" and len(parts) >= 2:
|
|
83
|
+
p = _safe(root, "justfile") or _safe(root, "Justfile")
|
|
84
|
+
if not p or not p.exists():
|
|
85
|
+
return CommandResolution("WARN", "command_manifest_missing", command, "justfile", "justfile missing")
|
|
86
|
+
targets = _just_targets(p.read_text(encoding="utf-8", errors="ignore"))
|
|
87
|
+
return CommandResolution("PASS", None, command, str(p.name), "recipe present") if parts[1] in targets else CommandResolution("FAIL", "unsupported_command", command, str(p.name), "recipe missing")
|
|
88
|
+
if parts[0] == "task" and len(parts) >= 2:
|
|
89
|
+
for name in ("Taskfile.yml", "Taskfile.yaml"):
|
|
90
|
+
p = _safe(root, name)
|
|
91
|
+
if p and p.exists():
|
|
92
|
+
try:
|
|
93
|
+
import yaml # type: ignore
|
|
94
|
+
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
95
|
+
except Exception:
|
|
96
|
+
data = _simple_taskfile_parse(p.read_text(encoding="utf-8", errors="ignore"))
|
|
97
|
+
targets = _taskfile_targets(data)
|
|
98
|
+
return CommandResolution("PASS", None, command, name, "task present") if parts[1] in targets else CommandResolution("FAIL", "unsupported_command", command, name, "task missing")
|
|
99
|
+
return CommandResolution("WARN", "command_manifest_missing", command, "Taskfile.yml", "Taskfile missing")
|
|
100
|
+
if parts[0] in {"pytest", "py.test"} or (len(parts) >= 3 and parts[0] == "python" and parts[1] == "-m" and parts[2] == "pytest"):
|
|
101
|
+
has_tests = any((root / name).exists() for name in ("tests", "test", "pytest.ini"))
|
|
102
|
+
if has_tests:
|
|
103
|
+
return CommandResolution("PASS", None, command, "tests", "pytest evidence present")
|
|
104
|
+
pyproject = _safe(root, "pyproject.toml")
|
|
105
|
+
requirements = list(root.glob("requirements*.txt"))
|
|
106
|
+
manifest_text = "\n".join(p.read_text(encoding="utf-8", errors="ignore") for p in ([pyproject] if pyproject and pyproject.exists() else []) + requirements)
|
|
107
|
+
if re.search(r"(?im)\bpytest\b", manifest_text):
|
|
108
|
+
return CommandResolution("PASS", None, command, "python dependency manifest", "pytest dependency present")
|
|
109
|
+
return CommandResolution("FAIL", "unsupported_command", command, "tests/pytest.ini/pyproject.toml", "pytest project evidence missing")
|
|
110
|
+
if parts[0] == "tox" and "-e" in parts:
|
|
111
|
+
env = parts[parts.index("-e") + 1] if parts.index("-e") + 1 < len(parts) else ""
|
|
112
|
+
p = _safe(root, "tox.ini")
|
|
113
|
+
if not p or not p.exists():
|
|
114
|
+
return CommandResolution("WARN", "command_check_inconclusive", command, "tox.ini", "tox.ini missing")
|
|
115
|
+
cp = configparser.ConfigParser(); cp.read(p)
|
|
116
|
+
raw = cp.get("tox", "envlist", fallback="")
|
|
117
|
+
if "{" in raw or "}" in raw or not raw:
|
|
118
|
+
return CommandResolution("WARN", "command_check_inconclusive", command, "tox.ini", "dynamic or missing envlist")
|
|
119
|
+
envs = {e.strip() for e in re.split(r"[,\n]", raw) if e.strip()}
|
|
120
|
+
return CommandResolution("PASS", None, command, "tox.ini", "env present") if env in envs else CommandResolution("FAIL", "unsupported_command", command, "tox.ini", "tox env missing")
|
|
121
|
+
if parts[0] == "nox" and "-s" in parts:
|
|
122
|
+
session = parts[parts.index("-s") + 1] if parts.index("-s") + 1 < len(parts) else ""
|
|
123
|
+
p = _safe(root, "noxfile.py")
|
|
124
|
+
if not p or not p.exists():
|
|
125
|
+
return CommandResolution("WARN", "command_manifest_missing", command, "noxfile.py", "noxfile missing")
|
|
126
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
127
|
+
if re.search(r"@nox\.session(?:\([^)]*\))?\s*\ndef\s+" + re.escape(session) + r"\b", text):
|
|
128
|
+
return CommandResolution("PASS", None, command, "noxfile.py", "session present")
|
|
129
|
+
return CommandResolution("WARN", "command_check_inconclusive", command, "noxfile.py", "dynamic or missing nox session")
|
|
130
|
+
return CommandResolution("WARN", "command_check_inconclusive", command, message="command parser unsupported")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _read_json_from_text(text: str) -> dict:
|
|
134
|
+
try:
|
|
135
|
+
return json.loads(text)
|
|
136
|
+
except Exception:
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _simple_taskfile_parse(text: str) -> dict:
|
|
141
|
+
tasks: dict[str, dict] = {}
|
|
142
|
+
in_tasks = False
|
|
143
|
+
for line in text.splitlines():
|
|
144
|
+
if re.match(r"^tasks:\s*$", line):
|
|
145
|
+
in_tasks = True; continue
|
|
146
|
+
if in_tasks:
|
|
147
|
+
m = re.match(r"^\s{2}([A-Za-z0-9_.-]+):", line)
|
|
148
|
+
if m: tasks[m.group(1)] = {}
|
|
149
|
+
return {"tasks": tasks}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast, json, re, sys, tomllib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from .ecosystems.python import PY_IMPORT_ALIASES
|
|
7
|
+
|
|
8
|
+
DEPENDENCY_SCHEMA_VERSION = "sourcepack.dependency_resolver.v1"
|
|
9
|
+
UNSUPPORTED_ECOSYSTEM_MANIFESTS = {"Cargo.toml", "go.mod", "pom.xml", "build.gradle", "settings.gradle"}
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class DependencyResolution:
|
|
13
|
+
verdict: str
|
|
14
|
+
reason_code: str | None
|
|
15
|
+
dependency: str
|
|
16
|
+
evidence_source: str | None = None
|
|
17
|
+
message: str = ""
|
|
18
|
+
def to_dict(self) -> dict:
|
|
19
|
+
return {"schema_version": DEPENDENCY_SCHEMA_VERSION, "verdict": self.verdict, "reason_code": self.reason_code, "dependency": self.dependency, "evidence_source": self.evidence_source, "message": self.message}
|
|
20
|
+
|
|
21
|
+
def normalize_python_package(name: str) -> str:
|
|
22
|
+
base = name.split(".")[0].replace("_", "-").lower()
|
|
23
|
+
return PY_IMPORT_ALIASES.get(base, base)
|
|
24
|
+
|
|
25
|
+
def normalize_js_package(spec: str) -> str:
|
|
26
|
+
if spec.startswith(".") or spec.startswith("/"):
|
|
27
|
+
return spec
|
|
28
|
+
parts = spec.split("/")
|
|
29
|
+
return "/".join(parts[:2]) if spec.startswith("@") and len(parts) >= 2 else parts[0]
|
|
30
|
+
|
|
31
|
+
def python_declared_dependencies(root: str | Path) -> dict[str, str]:
|
|
32
|
+
root = Path(root); found: dict[str, str] = {}
|
|
33
|
+
pyproject = root / "pyproject.toml"
|
|
34
|
+
if pyproject.exists():
|
|
35
|
+
try: data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
36
|
+
except Exception: data = {}
|
|
37
|
+
for dep in data.get("project", {}).get("dependencies", []) or []: found[_dep_name(dep)] = "pyproject.toml"
|
|
38
|
+
for group, deps in (data.get("project", {}).get("optional-dependencies", {}) or {}).items():
|
|
39
|
+
for dep in deps or []: found.setdefault(_dep_name(dep), f"pyproject.toml optional:{group}")
|
|
40
|
+
for group, gdata in (data.get("dependency-groups", {}) or {}).items():
|
|
41
|
+
for dep in (gdata if isinstance(gdata, list) else []): found.setdefault(_dep_name(str(dep)), f"pyproject.toml group:{group}")
|
|
42
|
+
poetry = data.get("tool", {}).get("poetry", {}).get("dependencies", {}) or {}
|
|
43
|
+
for dep in poetry:
|
|
44
|
+
if dep.lower() != "python": found[_dep_name(dep)] = "pyproject.toml poetry"
|
|
45
|
+
for req in root.glob("requirements*.txt"):
|
|
46
|
+
for line in req.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if line and not line.startswith("#") and not line.startswith("-"):
|
|
49
|
+
found[_dep_name(line)] = req.name
|
|
50
|
+
return found
|
|
51
|
+
|
|
52
|
+
def js_declared_dependencies(root: str | Path) -> dict[str, str]:
|
|
53
|
+
pj = Path(root) / "package.json"; found: dict[str, str] = {}
|
|
54
|
+
if not pj.exists(): return found
|
|
55
|
+
try: data = json.loads(pj.read_text(encoding="utf-8"))
|
|
56
|
+
except Exception: return found
|
|
57
|
+
for section in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
|
|
58
|
+
for dep in (data.get(section) or {}): found[dep] = f"package.json {section}"
|
|
59
|
+
return found
|
|
60
|
+
|
|
61
|
+
def resolve_python_import(root: str | Path, imported: str, *, added_dependencies: set[str] | None = None) -> DependencyResolution:
|
|
62
|
+
root = Path(root); top = imported.split(".")[0]
|
|
63
|
+
if top in sys.stdlib_module_names: return DependencyResolution("PASS", None, imported, "python stdlib", "stdlib")
|
|
64
|
+
if (root / (top + ".py")).exists() or (root / top / "__init__.py").exists() or (root / "src" / top / "__init__.py").exists() or (root / "src" / (top + ".py")).exists(): return DependencyResolution("PASS", None, imported, "worktree", "local module")
|
|
65
|
+
pkg = normalize_python_package(imported); declared = python_declared_dependencies(root)
|
|
66
|
+
if pkg in (added_dependencies or set()): return DependencyResolution("WARN", "declared_dependency", pkg, "patch", "dependency added in same patch")
|
|
67
|
+
if pkg in declared:
|
|
68
|
+
source = declared[pkg]
|
|
69
|
+
if "optional:" in source or "group:" in source: return DependencyResolution("WARN", "dependency_scope_review", pkg, source, "declared outside runtime dependency scope")
|
|
70
|
+
return DependencyResolution("PASS", None, pkg, source, "declared")
|
|
71
|
+
return DependencyResolution("FAIL", "unsupported_dependency", pkg, None, "external dependency not declared")
|
|
72
|
+
|
|
73
|
+
def resolve_js_import(root: str | Path, spec: str) -> DependencyResolution:
|
|
74
|
+
root = Path(root); pkg = normalize_js_package(spec)
|
|
75
|
+
if pkg.startswith(".") or pkg.startswith("/"): return DependencyResolution("PASS", None, spec, "relative import", "local relative import")
|
|
76
|
+
declared = js_declared_dependencies(root)
|
|
77
|
+
if pkg in declared:
|
|
78
|
+
src = declared[pkg]
|
|
79
|
+
if "devDependencies" in src: return DependencyResolution("WARN", "dependency_scope_review", pkg, src, "devDependency requires scope review")
|
|
80
|
+
return DependencyResolution("PASS", None, pkg, src, "declared")
|
|
81
|
+
if spec.startswith(("@/", "~/")):
|
|
82
|
+
return DependencyResolution("WARN", "js_alias_uncertain", spec, "tsconfig.json", "alias requires bounded resolver")
|
|
83
|
+
return DependencyResolution("FAIL", "unsupported_dependency", pkg, None, "package dependency not declared")
|
|
84
|
+
|
|
85
|
+
def unsupported_ecosystems(root: str | Path) -> list[DependencyResolution]:
|
|
86
|
+
root = Path(root); return [DependencyResolution("WARN", "unsupported_ecosystem", m.name, m.name, "ecosystem detected but not semantically resolved") for m in root.iterdir() if m.name in UNSUPPORTED_ECOSYSTEM_MANIFESTS]
|
|
87
|
+
|
|
88
|
+
def imports_from_python_source(text: str) -> set[str]:
|
|
89
|
+
out = set()
|
|
90
|
+
try: tree = ast.parse(text)
|
|
91
|
+
except SyntaxError: return out
|
|
92
|
+
for node in ast.walk(tree):
|
|
93
|
+
if isinstance(node, ast.Import): out.update(alias.name for alias in node.names)
|
|
94
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: out.add(node.module)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
def _dep_name(spec: str) -> str:
|
|
98
|
+
return re.split(r"[<>=!~;\[\s]", str(spec).strip(), 1)[0].replace("_", "-").lower()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PatchFileChange:
|
|
9
|
+
path: str
|
|
10
|
+
old_path: str | None
|
|
11
|
+
new_file: bool = False
|
|
12
|
+
deleted_file: bool = False
|
|
13
|
+
added_lines: list[str] | None = None
|
|
14
|
+
diff_lines: list[str] | None = None
|
|
15
|
+
unsafe_path: bool = False
|
|
16
|
+
operation: str = "modify"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_diff_path(path: str) -> tuple[str, bool]:
|
|
20
|
+
raw = path.strip().replace("\\", "/")
|
|
21
|
+
if raw.startswith("a/") or raw.startswith("b/"):
|
|
22
|
+
raw = raw[2:]
|
|
23
|
+
if not raw or raw in {"a/", "b/"}:
|
|
24
|
+
return raw, True
|
|
25
|
+
if raw.startswith("/") or re.match(r"^[A-Za-z]:/", raw):
|
|
26
|
+
return raw, True
|
|
27
|
+
parts: list[str] = []
|
|
28
|
+
unsafe = False
|
|
29
|
+
for part in PurePosixPath(raw).parts:
|
|
30
|
+
if part in {"", "."}:
|
|
31
|
+
continue
|
|
32
|
+
if part == "..":
|
|
33
|
+
if not parts:
|
|
34
|
+
unsafe = True
|
|
35
|
+
else:
|
|
36
|
+
parts.pop()
|
|
37
|
+
continue
|
|
38
|
+
parts.append(part)
|
|
39
|
+
return "/".join(parts), unsafe
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_unified_diff(text: str) -> list[PatchFileChange]:
|
|
43
|
+
changes: list[PatchFileChange] = []
|
|
44
|
+
current: PatchFileChange | None = None
|
|
45
|
+
old_path: str | None = None
|
|
46
|
+
new_path: str | None = None
|
|
47
|
+
new_file = False
|
|
48
|
+
deleted_file = False
|
|
49
|
+
operation = "modify"
|
|
50
|
+
|
|
51
|
+
malformed = False
|
|
52
|
+
|
|
53
|
+
def clean(path: str) -> tuple[str, bool]:
|
|
54
|
+
path = path.strip().split("\t", 1)[0]
|
|
55
|
+
return normalize_diff_path(path)
|
|
56
|
+
|
|
57
|
+
def flush():
|
|
58
|
+
nonlocal current
|
|
59
|
+
if current is not None:
|
|
60
|
+
changes.append(current)
|
|
61
|
+
current = None
|
|
62
|
+
|
|
63
|
+
for line in text.splitlines():
|
|
64
|
+
if line.startswith("diff --git "):
|
|
65
|
+
flush(); old_path = new_path = None; new_file = deleted_file = False; operation = "modify"
|
|
66
|
+
parts = line.split()
|
|
67
|
+
if len(parts) >= 4:
|
|
68
|
+
old_path, old_unsafe = clean(parts[2]); new_path, new_unsafe = clean(parts[3])
|
|
69
|
+
if old_unsafe or new_unsafe:
|
|
70
|
+
malformed = True
|
|
71
|
+
else:
|
|
72
|
+
malformed = True
|
|
73
|
+
elif line.startswith("new file mode"):
|
|
74
|
+
new_file = True
|
|
75
|
+
elif line.startswith("deleted file mode"):
|
|
76
|
+
deleted_file = True
|
|
77
|
+
elif line.startswith("rename from "):
|
|
78
|
+
old_path, unsafe = clean(line.removeprefix("rename from "))
|
|
79
|
+
operation = "rename"
|
|
80
|
+
malformed = malformed or unsafe
|
|
81
|
+
elif line.startswith("rename to "):
|
|
82
|
+
new_path, unsafe = clean(line.removeprefix("rename to "))
|
|
83
|
+
operation = "rename"
|
|
84
|
+
malformed = malformed or unsafe
|
|
85
|
+
current = PatchFileChange(path=new_path or old_path or "", old_path=old_path, new_file=False, deleted_file=False, added_lines=[], diff_lines=[], unsafe_path=unsafe, operation=operation)
|
|
86
|
+
elif line.startswith("copy from "):
|
|
87
|
+
old_path, unsafe = clean(line.removeprefix("copy from "))
|
|
88
|
+
operation = "copy"
|
|
89
|
+
malformed = malformed or unsafe
|
|
90
|
+
elif line.startswith("copy to "):
|
|
91
|
+
new_path, unsafe = clean(line.removeprefix("copy to "))
|
|
92
|
+
operation = "copy"
|
|
93
|
+
malformed = malformed or unsafe
|
|
94
|
+
current = PatchFileChange(path=new_path or old_path or "", old_path=old_path, new_file=True, deleted_file=False, added_lines=[], diff_lines=[], unsafe_path=unsafe, operation=operation)
|
|
95
|
+
elif line.startswith("--- "):
|
|
96
|
+
val = line[4:].strip()
|
|
97
|
+
if val == "/dev/null":
|
|
98
|
+
old_path = None
|
|
99
|
+
else:
|
|
100
|
+
old_path, unsafe = clean(val)
|
|
101
|
+
malformed = malformed or unsafe
|
|
102
|
+
elif line.startswith("+++ "):
|
|
103
|
+
val = line[4:].strip()
|
|
104
|
+
if val == "/dev/null":
|
|
105
|
+
new_path = None
|
|
106
|
+
unsafe = False
|
|
107
|
+
else:
|
|
108
|
+
new_path, unsafe = clean(val)
|
|
109
|
+
malformed = malformed or unsafe
|
|
110
|
+
path = new_path or old_path or ""
|
|
111
|
+
current = PatchFileChange(path=path, old_path=old_path, new_file=new_file or old_path is None, deleted_file=deleted_file or new_path is None, added_lines=[], diff_lines=[], unsafe_path=unsafe, operation=operation)
|
|
112
|
+
elif line.startswith("@@ ") and current is None:
|
|
113
|
+
malformed = True
|
|
114
|
+
elif current is not None and line.startswith("+") and not line.startswith("+++"):
|
|
115
|
+
current.added_lines.append(line[1:])
|
|
116
|
+
current.diff_lines.append(line)
|
|
117
|
+
elif current is not None and (line.startswith("-") or line.startswith(" ") or line.startswith("@@")):
|
|
118
|
+
current.diff_lines.append(line)
|
|
119
|
+
flush()
|
|
120
|
+
if malformed:
|
|
121
|
+
changes.append(PatchFileChange(path="", old_path=None, added_lines=[], diff_lines=[], unsafe_path=True))
|
|
122
|
+
return changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
UNSUPPORTED_ECOSYSTEM_FILES = {
|
|
4
|
+
"Cargo.toml": "Rust/Cargo",
|
|
5
|
+
"go.mod": "Go modules",
|
|
6
|
+
"pom.xml": "Java/Maven",
|
|
7
|
+
"build.gradle": "Java/Gradle",
|
|
8
|
+
"Gemfile": "Ruby/Bundler",
|
|
9
|
+
"composer.json": "PHP/Composer",
|
|
10
|
+
"*.csproj": ".NET/NuGet",
|
|
11
|
+
"main.tf": "Terraform",
|
|
12
|
+
"flake.nix": "Nix",
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
PY_IMPORT_ALIASES: dict[str, str] = {
|
|
4
|
+
"yaml": "pyyaml",
|
|
5
|
+
"cv2": "opencv-python",
|
|
6
|
+
"pil": "pillow",
|
|
7
|
+
"sklearn": "scikit-learn",
|
|
8
|
+
"bs4": "beautifulsoup4",
|
|
9
|
+
"dotenv": "python-dotenv",
|
|
10
|
+
"jwt": "pyjwt",
|
|
11
|
+
"dateutil": "python-dateutil",
|
|
12
|
+
}
|
sourcepack/errors.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
class SourcePackError(Exception):
|
|
4
|
+
"""Base class for typed SourcePack core failures."""
|
|
5
|
+
|
|
6
|
+
class BaselineMissingError(SourcePackError):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class BaselineCorruptError(SourcePackError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class MalformedDiffError(SourcePackError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class UnsafePathError(SourcePackError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class UnsupportedEcosystemError(SourcePackError):
|
|
19
|
+
pass
|
sourcepack/evidence.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
EVIDENCE_SCHEMA_VERSION = "sourcepack.evidence.v1"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EvidenceClass(StrEnum):
|
|
11
|
+
TRUSTED_BASELINE = "trusted_baseline"
|
|
12
|
+
CURRENT_WORKTREE = "current_worktree"
|
|
13
|
+
DEPENDENCY_MANIFEST = "dependency_manifest"
|
|
14
|
+
COMMAND_MANIFEST = "command_manifest"
|
|
15
|
+
EXECUTION_LEDGER = "execution_ledger"
|
|
16
|
+
GIT_METADATA = "git_metadata"
|
|
17
|
+
PROMPT_CONTEXT = "prompt_context"
|
|
18
|
+
AI_ANSWER = "ai_answer"
|
|
19
|
+
USER_CONFIG = "user_config"
|
|
20
|
+
UNSUPPORTED = "unsupported"
|
|
21
|
+
NOT_CHECKED = "not_checked"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TRUST_LEVELS = {
|
|
25
|
+
EvidenceClass.TRUSTED_BASELINE: "trusted",
|
|
26
|
+
EvidenceClass.CURRENT_WORKTREE: "local_observation",
|
|
27
|
+
EvidenceClass.DEPENDENCY_MANIFEST: "local_manifest",
|
|
28
|
+
EvidenceClass.COMMAND_MANIFEST: "local_manifest",
|
|
29
|
+
EvidenceClass.EXECUTION_LEDGER: "local_execution_record",
|
|
30
|
+
EvidenceClass.GIT_METADATA: "local_metadata",
|
|
31
|
+
EvidenceClass.USER_CONFIG: "user_policy",
|
|
32
|
+
EvidenceClass.PROMPT_CONTEXT: "advisory",
|
|
33
|
+
EvidenceClass.AI_ANSWER: "advisory",
|
|
34
|
+
EvidenceClass.UNSUPPORTED: "unsupported",
|
|
35
|
+
EvidenceClass.NOT_CHECKED: "not_checked",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ENFORCEMENT_CAPABLE = {
|
|
39
|
+
EvidenceClass.TRUSTED_BASELINE,
|
|
40
|
+
EvidenceClass.CURRENT_WORKTREE,
|
|
41
|
+
EvidenceClass.DEPENDENCY_MANIFEST,
|
|
42
|
+
EvidenceClass.COMMAND_MANIFEST,
|
|
43
|
+
EvidenceClass.EXECUTION_LEDGER,
|
|
44
|
+
EvidenceClass.GIT_METADATA,
|
|
45
|
+
EvidenceClass.USER_CONFIG,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class EvidenceRecord:
|
|
51
|
+
evidence_class: str
|
|
52
|
+
evidence_source: str
|
|
53
|
+
trust_level: str
|
|
54
|
+
checked_status: str
|
|
55
|
+
missing_evidence: str | None = None
|
|
56
|
+
required_evidence_class: str | None = None
|
|
57
|
+
supports_claim: str | None = None
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
return asdict(self)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def normalize_evidence_class(value: str | EvidenceClass) -> EvidenceClass:
|
|
64
|
+
return value if isinstance(value, EvidenceClass) else EvidenceClass(str(value))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def make_evidence(evidence_class: str | EvidenceClass, evidence_source: str, checked_status: str = "checked", *, missing_evidence: str | None = None, required_evidence_class: str | EvidenceClass | None = None, supports_claim: str | None = None) -> EvidenceRecord:
|
|
68
|
+
cls = normalize_evidence_class(evidence_class)
|
|
69
|
+
req = normalize_evidence_class(required_evidence_class).value if required_evidence_class else None
|
|
70
|
+
return EvidenceRecord(cls.value, evidence_source, TRUST_LEVELS[cls], checked_status, missing_evidence, req, supports_claim)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def can_satisfy(evidence: EvidenceRecord | dict, required: str | EvidenceClass, claim: str | None = None) -> bool:
|
|
74
|
+
eclass = normalize_evidence_class(evidence["evidence_class"] if isinstance(evidence, dict) else evidence.evidence_class)
|
|
75
|
+
required_cls = normalize_evidence_class(required)
|
|
76
|
+
if eclass in {EvidenceClass.PROMPT_CONTEXT, EvidenceClass.AI_ANSWER, EvidenceClass.UNSUPPORTED, EvidenceClass.NOT_CHECKED}:
|
|
77
|
+
return False
|
|
78
|
+
if eclass != required_cls:
|
|
79
|
+
return False
|
|
80
|
+
if eclass == EvidenceClass.EXECUTION_LEDGER and claim not in {None, "local_execution"}:
|
|
81
|
+
return False
|
|
82
|
+
return eclass in ENFORCEMENT_CAPABLE
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def evidence_summary(records: Iterable[EvidenceRecord | dict]) -> dict:
|
|
86
|
+
checked: list[dict] = []
|
|
87
|
+
missing: list[dict] = []
|
|
88
|
+
advisory: list[dict] = []
|
|
89
|
+
not_checked: list[dict] = []
|
|
90
|
+
for rec in records:
|
|
91
|
+
item = rec if isinstance(rec, dict) else rec.to_dict()
|
|
92
|
+
cls = item.get("evidence_class")
|
|
93
|
+
status = item.get("checked_status")
|
|
94
|
+
if cls in {EvidenceClass.PROMPT_CONTEXT.value, EvidenceClass.AI_ANSWER.value}:
|
|
95
|
+
advisory.append(item)
|
|
96
|
+
elif cls == EvidenceClass.NOT_CHECKED.value or status == "not_checked":
|
|
97
|
+
not_checked.append(item)
|
|
98
|
+
elif item.get("missing_evidence") or status in {"missing", "unavailable"}:
|
|
99
|
+
missing.append(item)
|
|
100
|
+
else:
|
|
101
|
+
checked.append(item)
|
|
102
|
+
return {"schema_version": EVIDENCE_SCHEMA_VERSION, "checked_evidence": checked, "missing_evidence": missing, "advisory_evidence_ignored_for_enforcement": advisory, "not_checked": not_checked}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def attach_evidence_to_finding(finding: dict, evidence_class: str | EvidenceClass, evidence_source: str, checked_status: str = "checked", **kwargs) -> dict:
|
|
106
|
+
result = dict(finding)
|
|
107
|
+
rec = make_evidence(evidence_class, evidence_source, checked_status, **kwargs).to_dict()
|
|
108
|
+
result.update(rec)
|
|
109
|
+
return result
|