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/paths.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sourcepack_paths(repo: str | Path) -> dict[str, Path]:
|
|
7
|
+
root = Path(repo).resolve()
|
|
8
|
+
base = root / ".sourcepack"
|
|
9
|
+
baseline = base / "baseline"
|
|
10
|
+
prompt = base / "prompt"
|
|
11
|
+
reports = base / "reports"
|
|
12
|
+
return {
|
|
13
|
+
"root": root,
|
|
14
|
+
"base": base,
|
|
15
|
+
"current": base / "current", # legacy compatibility marker only
|
|
16
|
+
"baseline": baseline,
|
|
17
|
+
"packet": baseline / "packet",
|
|
18
|
+
"baseline_meta": baseline / "metadata.json",
|
|
19
|
+
"prompt_dir": prompt,
|
|
20
|
+
"prompt_packet": prompt / "packet",
|
|
21
|
+
"prompt_reality": prompt / "reality_map.json",
|
|
22
|
+
"prompt_instructions": prompt / "ai_instructions.md",
|
|
23
|
+
"reports": reports,
|
|
24
|
+
"archive": reports / "archive",
|
|
25
|
+
"reality": baseline / "reality_map.json",
|
|
26
|
+
"instructions": baseline / "ai_instructions.md",
|
|
27
|
+
"prompt": prompt / "prompt.md",
|
|
28
|
+
"state": base / "state",
|
|
29
|
+
"stale_marker": base / "state" / "baseline_stale.json",
|
|
30
|
+
"latest_json": reports / "latest.json",
|
|
31
|
+
"latest_md": reports / "latest.md",
|
|
32
|
+
"latest_html": reports / "latest.html",
|
|
33
|
+
"latest_diff_json": reports / "latest_diff.json",
|
|
34
|
+
"latest_prompt_json": reports / "latest_prompt.json",
|
|
35
|
+
"latest_baseline_json": reports / "latest_baseline.json",
|
|
36
|
+
"builds": baseline / "builds",
|
|
37
|
+
"active_pointer": baseline / "active.json",
|
|
38
|
+
"baseline_lock": base / "state" / "baseline.lock",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ensure_sourcepack_dirs(repo: str | Path) -> dict[str, Path]:
|
|
43
|
+
paths = sourcepack_paths(repo)
|
|
44
|
+
paths["baseline"].mkdir(parents=True, exist_ok=True)
|
|
45
|
+
paths["prompt_dir"].mkdir(parents=True, exist_ok=True)
|
|
46
|
+
paths["current"].mkdir(parents=True, exist_ok=True)
|
|
47
|
+
paths["reports"].mkdir(parents=True, exist_ok=True)
|
|
48
|
+
paths["archive"].mkdir(parents=True, exist_ok=True)
|
|
49
|
+
paths["state"].mkdir(parents=True, exist_ok=True)
|
|
50
|
+
return paths
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ensure_gitignore_entry(repo: str | Path) -> tuple[bool, str | None]:
|
|
54
|
+
path = Path(repo) / ".gitignore"
|
|
55
|
+
try:
|
|
56
|
+
if not path.exists():
|
|
57
|
+
path.write_text(".sourcepack/\n", encoding="utf-8")
|
|
58
|
+
return True, None
|
|
59
|
+
data = path.read_bytes()
|
|
60
|
+
text = data.decode("utf-8")
|
|
61
|
+
if any(line.strip() in {".sourcepack", ".sourcepack/"} for line in text.splitlines()):
|
|
62
|
+
return False, None
|
|
63
|
+
newline = "\r\n" if b"\r\n" in data else "\n"
|
|
64
|
+
addition = ("" if text.endswith(("\n", "\r\n")) or not text else newline) + ".sourcepack/" + newline
|
|
65
|
+
path.write_text(text + addition, encoding="utf-8", newline="")
|
|
66
|
+
return True, None
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
return False, str(exc)
|
sourcepack/policy.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
class PolicyMode(StrEnum):
|
|
6
|
+
LOCAL = "local"
|
|
7
|
+
STRICT = "strict"
|
|
8
|
+
CI = "ci"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_policy_mode(value: PolicyMode | str | None) -> PolicyMode:
|
|
12
|
+
if isinstance(value, PolicyMode):
|
|
13
|
+
return value
|
|
14
|
+
if value is None:
|
|
15
|
+
return PolicyMode.LOCAL
|
|
16
|
+
text = str(value).lower().strip()
|
|
17
|
+
if text in {"ci", "--ci"}:
|
|
18
|
+
return PolicyMode.CI
|
|
19
|
+
if text in {"strict", "--strict"}:
|
|
20
|
+
return PolicyMode.STRICT
|
|
21
|
+
return PolicyMode.LOCAL
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def commit_policy(verdict: str) -> str | None:
|
|
25
|
+
if verdict == "WARN":
|
|
26
|
+
return "allowed locally, blocked in strict mode."
|
|
27
|
+
if verdict == "FAIL":
|
|
28
|
+
return "blocked unless explicitly bypassed."
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def exit_code(verdict: str, mode: PolicyMode | str | None = None) -> int:
|
|
33
|
+
mode = normalize_policy_mode(mode)
|
|
34
|
+
if verdict == "FAIL":
|
|
35
|
+
return 1
|
|
36
|
+
if verdict == "WARN" and mode in {PolicyMode.STRICT, PolicyMode.CI}:
|
|
37
|
+
return 1
|
|
38
|
+
return 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
REASON_CODE_VOCABULARY_VERSION = "reason_codes.v1"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReasonCode(StrEnum):
|
|
9
|
+
BASELINE_MISSING = "baseline_missing"
|
|
10
|
+
BASELINE_STALE = "baseline_stale"
|
|
11
|
+
BASELINE_CORRUPT = "baseline_corrupt"
|
|
12
|
+
MISSING_FILE = "missing_file"
|
|
13
|
+
NEW_FILE = "new_file"
|
|
14
|
+
DELETED_FILE = "deleted_file"
|
|
15
|
+
UNSUPPORTED_DEPENDENCY = "unsupported_dependency"
|
|
16
|
+
DECLARED_DEPENDENCY = "declared_dependency"
|
|
17
|
+
DECLARED_COMMAND = "declared_command"
|
|
18
|
+
UNSUPPORTED_COMMAND = "unsupported_command"
|
|
19
|
+
UNSAFE_PATH = "unsafe_path"
|
|
20
|
+
PATH_ESCAPE = "path_escape"
|
|
21
|
+
PROTECTED_ARTIFACT = "protected_artifact"
|
|
22
|
+
GIT_PATH_MODIFICATION = "git_path_modification"
|
|
23
|
+
BINARY_DIFF = "binary_diff"
|
|
24
|
+
MALFORMED_DIFF = "malformed_diff"
|
|
25
|
+
UNSUPPORTED_ECOSYSTEM = "unsupported_ecosystem"
|
|
26
|
+
DIRTY_WORKTREE = "dirty_worktree"
|
|
27
|
+
BASELINE_LOCKED = "baseline_locked"
|
|
28
|
+
BASELINE_FAILED = "baseline_failed"
|
|
29
|
+
GIT_UNAVAILABLE = "git_unavailable"
|
|
30
|
+
NO_GIT_REPO = "no_git_repo"
|
|
31
|
+
NO_DIFF = "no_diff"
|
|
32
|
+
REPO_NOT_DIRECTORY = "repo_not_directory"
|
|
33
|
+
GITIGNORE_UNWRITABLE = "gitignore_unwritable"
|
|
34
|
+
PROMPT_CONTEXT_FAILED = "prompt_context_failed"
|
|
35
|
+
CLIPBOARD_UNAVAILABLE = "clipboard_unavailable"
|
|
36
|
+
HOOK_INSTALL_FAILED = "hook_install_failed"
|
|
37
|
+
HYGIENE_HOOKS_DEFERRED = "hygiene_hooks_deferred"
|
|
38
|
+
BASELINE_INVENTORY_MISSING = "baseline_inventory_missing"
|
|
39
|
+
WORKFLOW_CHANGE = "workflow_change"
|
|
40
|
+
UNSUPPORTED_RENAME_COPY = "unsupported_rename_copy"
|
|
41
|
+
DEPENDENCY_MANIFEST_UNCERTAIN = "dependency_manifest_uncertain"
|
|
42
|
+
COMMAND_MANIFEST_UNCERTAIN = "command_manifest_uncertain"
|
|
43
|
+
COMMAND_MANIFEST_MISSING = "command_manifest_missing"
|
|
44
|
+
COMMAND_CHECK_INCONCLUSIVE = "command_check_inconclusive"
|
|
45
|
+
DEPENDENCY_SCOPE_REVIEW = "dependency_scope_review"
|
|
46
|
+
JS_ALIAS_UNCERTAIN = "js_alias_uncertain"
|
|
47
|
+
EXECUTION_EVIDENCE_MISSING = "execution_evidence_missing"
|
|
48
|
+
EXECUTION_EVIDENCE_PRESENT = "execution_evidence_present"
|
|
49
|
+
EXECUTION_FAILED = "execution_failed"
|
|
50
|
+
EXECUTION_INCONCLUSIVE = "execution_inconclusive"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_CANONICAL = {code.value for code in ReasonCode}
|
|
54
|
+
_ALIASES = {
|
|
55
|
+
"baseline-corrupt": ReasonCode.BASELINE_CORRUPT.value,
|
|
56
|
+
"baseline-missing": ReasonCode.BASELINE_MISSING.value,
|
|
57
|
+
"baseline-stale": ReasonCode.BASELINE_STALE.value,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def normalize_reason_code(code: str) -> str:
|
|
62
|
+
normalized = str(code).strip().lower().replace("-", "_").replace(" ", "_")
|
|
63
|
+
normalized = _ALIASES.get(normalized, normalized)
|
|
64
|
+
return normalized
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_canonical_reason_code(code: str) -> bool:
|
|
68
|
+
return normalize_reason_code(code) in _CANONICAL
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def canonical_reason_codes() -> tuple[str, ...]:
|
|
72
|
+
return tuple(sorted(_CANONICAL))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from xml.sax.saxutils import escape as xml_escape
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _html_escape(value: object) -> str:
|
|
7
|
+
return xml_escape("" if value is None else str(value), {'"': '"', "'": '''})
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _report_badge_class(verdict: str) -> str:
|
|
11
|
+
return {"PASS": "pass", "WARN": "warn", "FAIL": "fail"}.get(verdict, "warn")
|
|
12
|
+
|
|
13
|
+
def render_report_html(report: dict) -> str:
|
|
14
|
+
verdict = str(report.get("verdict", "WARN"))
|
|
15
|
+
badge = _report_badge_class(verdict)
|
|
16
|
+
findings = report.get("findings", []) if isinstance(report.get("findings"), list) else []
|
|
17
|
+
raw_json_path = report.get("report_path") or ".sourcepack/reports/latest.json"
|
|
18
|
+
baseline_path = report.get("baseline_packet_path") or (report.get("baseline") or {}).get("packet_path") if isinstance(report.get("baseline"), dict) else report.get("baseline_packet_path")
|
|
19
|
+
|
|
20
|
+
def finding_rows(items: list[dict]) -> str:
|
|
21
|
+
if not items:
|
|
22
|
+
return '<tr><td colspan="5" class="muted">None.</td></tr>'
|
|
23
|
+
rows = []
|
|
24
|
+
for f in items:
|
|
25
|
+
rows.append(
|
|
26
|
+
"<tr>"
|
|
27
|
+
f"<td><code>{_html_escape(f.get('id'))}</code></td>"
|
|
28
|
+
f"<td><span class='severity {_html_escape(f.get('severity'))}'>{_html_escape(f.get('severity'))}</span></td>"
|
|
29
|
+
f"<td>{_html_escape(f.get('path') or '—')}</td>"
|
|
30
|
+
f"<td>{_html_escape(f.get('message'))}</td>"
|
|
31
|
+
f"<td>{_html_escape(f.get('suggestion') or f.get('evidence') or '—')}</td>"
|
|
32
|
+
"</tr>"
|
|
33
|
+
)
|
|
34
|
+
return "\n".join(rows)
|
|
35
|
+
|
|
36
|
+
checked = "".join(f"<li>{_html_escape(item)}</li>" for item in report.get("checked_categories", [])) or "<li>None recorded.</li>"
|
|
37
|
+
not_checked = "".join(f"<li>{_html_escape(item)}</li>" for item in report.get("not_checked", [])) or "<li>None recorded.</li>"
|
|
38
|
+
affected = sorted({str(f.get("path")) for f in findings if f.get("path")})
|
|
39
|
+
affected_html = "".join(f"<li><code>{_html_escape(path)}</code></li>" for path in affected) or "<li>No affected file paths recorded.</li>"
|
|
40
|
+
missing = [f for f in findings if f.get("id") in {"missing_file", "unsupported_dependency", "unsupported_command", "unsupported_ecosystem", "js_alias_uncertain", "dependency_manifest_uncertain"} or f.get("category") == "uncertainty"]
|
|
41
|
+
fixes = [f for f in findings if f.get("suggestion")]
|
|
42
|
+
missing_html = "".join(f"<li><code>{_html_escape(f.get('id'))}</code>: {_html_escape(f.get('message'))}</li>" for f in missing) or "<li>No missing evidence recorded.</li>"
|
|
43
|
+
fixes_html = "".join(f"<li>{_html_escape(f.get('suggestion'))}</li>" for f in fixes) or "<li>No suggested fixes recorded.</li>"
|
|
44
|
+
generated = _html_escape(report.get("generated_at", "unknown"))
|
|
45
|
+
return f"""<!doctype html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="utf-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
50
|
+
<title>SourcePack Report - {verdict}</title>
|
|
51
|
+
<style>
|
|
52
|
+
:root {{ color-scheme: light dark; --bg:#0f172a; --panel:#111827; --text:#e5e7eb; --muted:#94a3b8; --line:#334155; --pass:#16a34a; --warn:#d97706; --fail:#dc2626; }}
|
|
53
|
+
body {{ margin:0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }}
|
|
54
|
+
main {{ max-width:1100px; margin:0 auto; padding:32px 20px 56px; }}
|
|
55
|
+
header, section {{ background:rgba(17,24,39,.92); border:1px solid var(--line); border-radius:18px; padding:22px; margin:16px 0; box-shadow:0 20px 50px rgba(0,0,0,.22); }}
|
|
56
|
+
h1 {{ margin:0 0 8px; font-size:32px; }}
|
|
57
|
+
h2 {{ margin-top:0; }}
|
|
58
|
+
.badge {{ display:inline-block; padding:8px 14px; border-radius:999px; font-weight:800; letter-spacing:.04em; }}
|
|
59
|
+
.badge.pass {{ background:var(--pass); }} .badge.warn {{ background:var(--warn); }} .badge.fail {{ background:var(--fail); }}
|
|
60
|
+
.grid {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:14px; }}
|
|
61
|
+
.card {{ border:1px solid var(--line); border-radius:14px; padding:14px; background:rgba(15,23,42,.72); }}
|
|
62
|
+
.label {{ color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.08em; }}
|
|
63
|
+
.value {{ margin-top:6px; font-weight:650; overflow-wrap:anywhere; }}
|
|
64
|
+
table {{ width:100%; border-collapse:collapse; }} th, td {{ text-align:left; border-bottom:1px solid var(--line); padding:10px; vertical-align:top; }} th {{ color:var(--muted); font-size:13px; }}
|
|
65
|
+
code {{ color:#bfdbfe; }} .muted {{ color:var(--muted); }} .severity.error {{ color:#fca5a5; }} .severity.warn {{ color:#fcd34d; }} .severity.info {{ color:#93c5fd; }}
|
|
66
|
+
</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body><main>
|
|
69
|
+
<header>
|
|
70
|
+
<span class="badge {badge}">{_html_escape(report.get('light') or verdict)}</span>
|
|
71
|
+
<h1>SourcePack local report</h1>
|
|
72
|
+
<p>{_html_escape(report.get('headline'))}</p>
|
|
73
|
+
<p class="muted">Generated {generated}</p>
|
|
74
|
+
</header>
|
|
75
|
+
<section class="grid">
|
|
76
|
+
<div class="card"><div class="label">Verdict</div><div class="value">{_html_escape(verdict)}</div></div>
|
|
77
|
+
<div class="card"><div class="label">Reason type</div><div class="value">{_html_escape(report.get('reason_type') or 'none')}</div></div>
|
|
78
|
+
<div class="card"><div class="label">Commit policy</div><div class="value">{_html_escape(report.get('commit_policy') or 'allowed.')}</div></div>
|
|
79
|
+
<div class="card"><div class="label">Raw JSON</div><div class="value"><code>{_html_escape(raw_json_path)}</code></div></div>
|
|
80
|
+
</section>
|
|
81
|
+
<section><h2>Reason codes</h2><table><thead><tr><th>Code</th><th>Severity</th><th>Path</th><th>Explanation</th><th>Evidence / fix</th></tr></thead><tbody>{finding_rows(findings)}</tbody></table></section>
|
|
82
|
+
<section class="grid"><div class="card"><h2>Affected files</h2><ul>{affected_html}</ul></div><div class="card"><h2>Evidence found</h2><ul>{checked}</ul></div><div class="card"><h2>Evidence missing</h2><ul>{missing_html}</ul></div><div class="card"><h2>Suggested fixes</h2><ul>{fixes_html}</ul></div></section>
|
|
83
|
+
<section><h2>Baseline and prompt trust</h2><p>SourcePack treats prompt context as helpful but non-authoritative. Diff checks are judged against the trusted local baseline packet.</p><div class="grid"><div class="card"><div class="label">Baseline state</div><div class="value">{_html_escape(report.get('baseline_state') or 'not recorded')}</div></div><div class="card"><div class="label">Baseline packet</div><div class="value"><code>{_html_escape(baseline_path or 'not recorded')}</code></div></div></div></section>
|
|
84
|
+
<section class="grid"><div class="card"><h2>Checked</h2><ul>{checked}</ul></div><div class="card"><h2>Not checked</h2><ul>{not_checked}</ul></div></section>
|
|
85
|
+
<section><h2>Execution evidence</h2><p>Execution evidence proves only that a command was run locally and records exit/output hashes; it does not prove correctness, security, or external API behavior.</p></section><section><h2>Safe next actions</h2><p>{_html_escape(report.get('next_action'))}</p></section>
|
|
86
|
+
</main></body></html>"""
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sourcepack import __version__
|
|
9
|
+
from sourcepack.paths import ensure_sourcepack_dirs
|
|
10
|
+
from sourcepack.reports.html import render_report_html
|
|
11
|
+
from sourcepack.reports.markdown import LIGHT_BY_VERDICT, render_traffic
|
|
12
|
+
from sourcepack.reason_codes import normalize_reason_code, is_canonical_reason_code
|
|
13
|
+
from sourcepack.evidence import attach_evidence_to_finding, evidence_summary, make_evidence
|
|
14
|
+
|
|
15
|
+
SEVERITY_ORDER = {"error": 0, "warn": 1, "info": 2}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def utc_now() -> str:
|
|
19
|
+
return datetime.now(timezone.utc).isoformat()
|
|
20
|
+
|
|
21
|
+
def normalized_finding(fid: str, severity: str, category: str, message: str, path: str | None = None, evidence: str | None = None, suggestion: str | None = None) -> dict:
|
|
22
|
+
code = normalize_reason_code(fid)
|
|
23
|
+
if severity in {"error", "warn"} and not is_canonical_reason_code(code):
|
|
24
|
+
raise ValueError(f"unknown SourcePack reason code: {fid}")
|
|
25
|
+
return {"id": code, "severity": severity, "category": category, "path": path, "message": message, "evidence": evidence, "suggestion": suggestion}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_finding_evidence(finding: dict) -> dict:
|
|
29
|
+
if finding.get("evidence_class"):
|
|
30
|
+
return finding
|
|
31
|
+
fid = str(finding.get("id") or "")
|
|
32
|
+
category = str(finding.get("category") or "")
|
|
33
|
+
source = str(finding.get("evidence") or finding.get("path") or category or fid)
|
|
34
|
+
if category == "dependency" or fid in {"unsupported_dependency", "declared_dependency", "dependency_scope_review"}:
|
|
35
|
+
status = "missing" if fid == "unsupported_dependency" else "partially_checked" if fid in {"declared_dependency", "dependency_scope_review"} else "checked"
|
|
36
|
+
return attach_evidence_to_finding(finding, "dependency_manifest", source, status, missing_evidence=source if status == "missing" else None, required_evidence_class="dependency_manifest")
|
|
37
|
+
if category == "command" or fid in {"unsupported_command", "declared_command", "command_manifest_missing", "command_check_inconclusive", "command_manifest_uncertain"}:
|
|
38
|
+
status = "missing" if fid in {"unsupported_command", "command_manifest_missing"} else "partially_checked" if fid in {"declared_command", "command_check_inconclusive", "command_manifest_uncertain"} else "checked"
|
|
39
|
+
return attach_evidence_to_finding(finding, "command_manifest", source, status, missing_evidence=source if status == "missing" else None, required_evidence_class="command_manifest")
|
|
40
|
+
if category == "execution" or fid.startswith("execution_"):
|
|
41
|
+
status = "checked" if fid == "execution_evidence_present" else "unavailable" if fid == "execution_evidence_missing" else "partially_checked"
|
|
42
|
+
return attach_evidence_to_finding(finding, "execution_ledger", source, status, missing_evidence=source if status == "unavailable" else None, required_evidence_class="execution_ledger", supports_claim="local_execution")
|
|
43
|
+
if category in {"baseline", "file"} or fid in {"missing_file", "baseline_missing", "baseline_corrupt", "baseline_stale", "baseline_inventory_missing"}:
|
|
44
|
+
status = "missing" if fid in {"missing_file", "baseline_missing", "baseline_corrupt", "baseline_inventory_missing"} else "checked"
|
|
45
|
+
return attach_evidence_to_finding(finding, "trusted_baseline", source, status, missing_evidence=source if status == "missing" else None, required_evidence_class="trusted_baseline")
|
|
46
|
+
if category == "artifact" or fid in {"protected_artifact", "git_path_modification"}:
|
|
47
|
+
eclass = "git_metadata" if fid == "git_path_modification" else "trusted_baseline"
|
|
48
|
+
return attach_evidence_to_finding(finding, eclass, source, "checked", required_evidence_class=eclass)
|
|
49
|
+
if fid in {"unsupported_ecosystem", "binary_diff", "path_escape", "unsafe_path"}:
|
|
50
|
+
return attach_evidence_to_finding(finding, "unsupported", source, "unsupported", missing_evidence=source, required_evidence_class="current_worktree")
|
|
51
|
+
return finding
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def traffic_report(verdict: str, headline: str | None = None, findings: list[dict] | None = None, checked_categories: list[str] | None = None, next_action: str | None = None, report_path: str = ".sourcepack/reports/latest.json", reason_type: str | None = None, not_checked: list[str] | None = None) -> dict:
|
|
55
|
+
findings = [normalize_finding_evidence(f) for f in (findings or [])]
|
|
56
|
+
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f.get("severity", "info"), 9), f.get("id", ""), f.get("path") or ""))
|
|
57
|
+
blockers = [f for f in findings if f.get("severity") == "error"]
|
|
58
|
+
warnings = [f for f in findings if f.get("severity") == "warn"]
|
|
59
|
+
light = LIGHT_BY_VERDICT.get(verdict, "YELLOW LIGHT")
|
|
60
|
+
if reason_type is None:
|
|
61
|
+
reason_type = "blocker" if verdict == "FAIL" else "review" if warnings else "none"
|
|
62
|
+
if any(f.get("category") in {"uncertainty", "tooling"} for f in warnings):
|
|
63
|
+
reason_type = "uncertainty" if any(f.get("category") == "uncertainty" for f in warnings) else "tooling"
|
|
64
|
+
if headline is None:
|
|
65
|
+
if verdict == "WARN" and reason_type == "uncertainty":
|
|
66
|
+
headline = "SourcePack could not fully evaluate this change."
|
|
67
|
+
elif verdict == "WARN" and reason_type == "tooling":
|
|
68
|
+
headline = "SourcePack tooling degraded."
|
|
69
|
+
else:
|
|
70
|
+
headline = {"PASS": "good to continue.", "WARN": "review before continuing.", "FAIL": "stop before trusting this output."}.get(verdict, "review before continuing.")
|
|
71
|
+
next_action = next_action or ("ask the AI to revise using only files, dependencies, and commands confirmed by SourcePack." if verdict == "FAIL" else "review the listed items before continuing." if verdict == "WARN" else "continue.")
|
|
72
|
+
commit_policy = None
|
|
73
|
+
if verdict == "WARN":
|
|
74
|
+
commit_policy = "allowed locally, blocked in strict mode."
|
|
75
|
+
elif verdict == "FAIL":
|
|
76
|
+
commit_policy = "blocked unless explicitly bypassed."
|
|
77
|
+
checked_categories = checked_categories or []
|
|
78
|
+
not_checked = not_checked or ["runtime behavior", "semantic correctness", "security", "external services"]
|
|
79
|
+
records = []
|
|
80
|
+
for category in checked_categories:
|
|
81
|
+
eclass = "trusted_baseline" if "baseline" in category else "dependency_manifest" if "import" in category.lower() else "command_manifest" if "command" in category.lower() else "current_worktree"
|
|
82
|
+
records.append(make_evidence(eclass, category, "checked"))
|
|
83
|
+
for category in not_checked:
|
|
84
|
+
records.append(make_evidence("not_checked", category, "not_checked"))
|
|
85
|
+
for f in findings:
|
|
86
|
+
if f.get("evidence_class"):
|
|
87
|
+
records.append(f)
|
|
88
|
+
evidence = evidence_summary(records)
|
|
89
|
+
partial = sorted({f["category"] for f in findings if f.get("checked_status") == "partially_checked"} | ({"execution_claim_check"} if any(f.get("category") == "execution" for f in findings) else set()))
|
|
90
|
+
checked_names = sorted(set(checked_categories) | {f["category"] for f in findings if f.get("checked_status") == "checked"})
|
|
91
|
+
confidence_summary = {"basis": "local evidence coverage, not AI confidence", "checked": checked_names, "partially_checked": partial, "not_checked": not_checked, "limitations": ["SourcePack does not prove code correctness", "SourcePack does not prove security", "SourcePack does not verify external API behavior unless local evidence exists"]}
|
|
92
|
+
return {"schema_version": "traffic_report.v1", "sourcepack_version": __version__, "verdict": verdict, "light": light, "headline": headline, "reason_type": reason_type, "commit_policy": commit_policy, "blockers": blockers, "warnings": warnings, "uncertainties": [f for f in warnings if f.get("category") == "uncertainty"], "checked_categories": checked_names, "checked": checked_names, "partially_checked": partial, "unavailable_evidence": evidence["missing_evidence"], "unsupported_evidence": [f for f in findings if f.get("id") == "unsupported_ecosystem"], "not_checked": not_checked, "confidence_summary": confidence_summary, "evidence": evidence, "next_action": next_action, "report_path": report_path, "findings": findings}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _write_optional_report_file(path: Path, content: str) -> None:
|
|
96
|
+
try:
|
|
97
|
+
path.write_text(content, encoding="utf-8")
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
print(f"WARNING: could not write SourcePack report artifact {path}: {exc}", file=sys.stderr)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_user_report(repo: str | Path, report: dict, stem: str = "report") -> None:
|
|
103
|
+
paths = ensure_sourcepack_dirs(repo)
|
|
104
|
+
full = dict(report)
|
|
105
|
+
full.setdefault("sourcepack_version", __version__)
|
|
106
|
+
full.setdefault("schema_version", "traffic_report.v1")
|
|
107
|
+
full["generated_at"] = utc_now()
|
|
108
|
+
json_text = json.dumps(full, indent=2)
|
|
109
|
+
md_text = render_traffic(full, verbose=True)
|
|
110
|
+
paths["latest_json"].write_text(json_text, encoding="utf-8")
|
|
111
|
+
_write_optional_report_file(paths["latest_md"], md_text)
|
|
112
|
+
try:
|
|
113
|
+
html_text = render_report_html(full)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
print(f"WARNING: could not render SourcePack HTML report: {exc}", file=sys.stderr)
|
|
116
|
+
else:
|
|
117
|
+
_write_optional_report_file(paths["latest_html"], html_text)
|
|
118
|
+
typed = paths.get(f"latest_{stem}_json")
|
|
119
|
+
if typed is not None:
|
|
120
|
+
_write_optional_report_file(typed, json_text)
|
|
121
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
122
|
+
_write_optional_report_file(paths["archive"] / f"{ts}_{stem}.json", json_text)
|
|
123
|
+
_write_optional_report_file(paths["archive"] / f"{ts}_{stem}.md", md_text)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
LIGHT_BY_VERDICT = {"PASS": "GREEN LIGHT", "WARN": "YELLOW LIGHT", "FAIL": "RED LIGHT"}
|
|
4
|
+
SEVERITY_ORDER = {"error": 0, "warn": 1, "info": 2}
|
|
5
|
+
|
|
6
|
+
def render_traffic(report: dict, verbose: bool = False) -> str:
|
|
7
|
+
verdict = report.get("verdict", "WARN")
|
|
8
|
+
lines = [f"Verdict: {verdict}", f"{report.get('light', LIGHT_BY_VERDICT.get(verdict, 'YELLOW LIGHT'))}: {report.get('headline', '')}", ""]
|
|
9
|
+
if report.get("reason_type"):
|
|
10
|
+
lines.append(f"Reason type: {report.get('reason_type')}")
|
|
11
|
+
lines.append(f"Commit policy: {report.get('commit_policy') or 'allowed.'}")
|
|
12
|
+
lines.append("")
|
|
13
|
+
if verdict == "PASS":
|
|
14
|
+
info = [f for f in report.get("findings", []) if f.get("severity") == "info"]
|
|
15
|
+
lines.append(info[0]["message"] if info else "No unsupported project claims or patch assumptions detected.")
|
|
16
|
+
if report.get("checked_categories"):
|
|
17
|
+
lines.extend(["", "Checked:", ""])
|
|
18
|
+
lines.extend(f"- {item}" for item in report.get("checked_categories", []))
|
|
19
|
+
if report.get("not_checked"):
|
|
20
|
+
lines.extend(["", "Not checked:", ""])
|
|
21
|
+
lines.extend(f"- {item}" for item in report.get("not_checked", []))
|
|
22
|
+
elif verdict == "WARN":
|
|
23
|
+
lines.append("SourcePack found review or uncertainty items, but no clear unsupported blocker.")
|
|
24
|
+
review = [f for f in report.get("warnings", []) if f.get("category") != "uncertainty"]
|
|
25
|
+
uncertain = [f for f in report.get("warnings", []) if f.get("category") == "uncertainty"]
|
|
26
|
+
if review:
|
|
27
|
+
lines.extend(["", "Review warnings:", ""])
|
|
28
|
+
shown = review if verbose else review[:3]
|
|
29
|
+
lines.extend(f"- {f.get('id')}: {f.get('message')}" for f in shown)
|
|
30
|
+
if uncertain:
|
|
31
|
+
lines.extend(["", "Uncertainties:", ""])
|
|
32
|
+
shown = uncertain if verbose else uncertain[:3]
|
|
33
|
+
lines.extend(f"- {f.get('id')}: {f.get('message')}" for f in shown)
|
|
34
|
+
lines.extend(["", f"Next action: {report.get('next_action')}"])
|
|
35
|
+
else:
|
|
36
|
+
lines.append("SourcePack found missing files, unsupported dependencies, unsupported commands, or unsupported capabilities.")
|
|
37
|
+
if report.get("blockers"):
|
|
38
|
+
lines.extend(["", "Blockers:", ""])
|
|
39
|
+
shown = report.get("blockers", []) if verbose else report.get("blockers", [])[:3]
|
|
40
|
+
lines.extend(f"- {f.get('id')}: {f.get('message')}" for f in shown)
|
|
41
|
+
review = [f for f in report.get("warnings", []) if f.get("category") != "uncertainty"]
|
|
42
|
+
uncertain = [f for f in report.get("warnings", []) if f.get("category") == "uncertainty"]
|
|
43
|
+
if review:
|
|
44
|
+
lines.extend(["", "Review warnings:", ""])
|
|
45
|
+
shown = review if verbose else review[:3]
|
|
46
|
+
lines.extend(f"- {f.get('id')}: {f.get('message')}" for f in shown)
|
|
47
|
+
if uncertain:
|
|
48
|
+
lines.extend(["", "Uncertainties:", ""])
|
|
49
|
+
shown = uncertain if verbose else uncertain[:3]
|
|
50
|
+
lines.extend(f"- {f.get('id')}: {f.get('message')}" for f in shown)
|
|
51
|
+
lines.extend(["", f"Next action: {report.get('next_action')}"])
|
|
52
|
+
if verdict != "PASS":
|
|
53
|
+
if report.get("checked_categories"):
|
|
54
|
+
lines.extend(["", "Checked:", ""])
|
|
55
|
+
lines.extend(f"- {item}" for item in report.get("checked_categories", []))
|
|
56
|
+
if report.get("not_checked"):
|
|
57
|
+
lines.extend(["", "Not checked:", ""])
|
|
58
|
+
lines.extend(f"- {item}" for item in report.get("not_checked", []))
|
|
59
|
+
lines.extend(["", f"Report path: {report.get('report_path', '.sourcepack/reports/latest.json')}"])
|
|
60
|
+
return "\n".join(lines) + "\n"
|
|
61
|
+
|
sourcepack/schemas.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .reason_codes import ReasonCode
|
|
8
|
+
|
|
9
|
+
BASELINE_SCHEMA_VERSION = "baseline_pointer.v1"
|
|
10
|
+
JUDGMENT_REPORT_SCHEMA_VERSION = "traffic_report.v1"
|
|
11
|
+
PROMPT_CONTEXT_SCHEMA_VERSION = "prompt_context.v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Verdict(StrEnum):
|
|
15
|
+
PASS = "PASS"
|
|
16
|
+
WARN = "WARN"
|
|
17
|
+
FAIL = "FAIL"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Severity(StrEnum):
|
|
21
|
+
INFO = "info"
|
|
22
|
+
WARN = "warn"
|
|
23
|
+
ERROR = "error"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PolicyMode(StrEnum):
|
|
27
|
+
LOCAL = "local"
|
|
28
|
+
STRICT = "strict"
|
|
29
|
+
CI = "ci"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Finding:
|
|
34
|
+
code: ReasonCode | str
|
|
35
|
+
severity: Severity | str
|
|
36
|
+
path: str | None
|
|
37
|
+
message: str
|
|
38
|
+
evidence: str | None = None
|
|
39
|
+
suggested_fixes: list[str] = field(default_factory=list)
|
|
40
|
+
category: str | None = None
|
|
41
|
+
|
|
42
|
+
def to_report_dict(self) -> dict[str, Any]:
|
|
43
|
+
suggestion = self.suggested_fixes[0] if self.suggested_fixes else None
|
|
44
|
+
return {
|
|
45
|
+
"id": str(self.code),
|
|
46
|
+
"severity": str(self.severity),
|
|
47
|
+
"category": self.category,
|
|
48
|
+
"path": self.path,
|
|
49
|
+
"message": self.message,
|
|
50
|
+
"evidence": self.evidence,
|
|
51
|
+
"suggestion": suggestion,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Judgment:
|
|
57
|
+
verdict: Verdict | str
|
|
58
|
+
findings: list[Finding]
|
|
59
|
+
checked_categories: list[str]
|
|
60
|
+
not_checked: list[str]
|
|
61
|
+
reason_type: str | None
|
|
62
|
+
commit_policy: str | None
|
|
63
|
+
next_action: str
|