sembl-stack 0.1.0__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 (45) hide show
  1. sembl_stack/__init__.py +3 -0
  2. sembl_stack/adapters/__init__.py +0 -0
  3. sembl_stack/adapters/_redact.py +19 -0
  4. sembl_stack/adapters/base.py +179 -0
  5. sembl_stack/adapters/codegraph_cbm.py +95 -0
  6. sembl_stack/adapters/deploy_vercel.py +215 -0
  7. sembl_stack/adapters/execute_aider.py +115 -0
  8. sembl_stack/adapters/execute_claude.py +114 -0
  9. sembl_stack/adapters/execute_mock.py +53 -0
  10. sembl_stack/adapters/execute_opencode.py +114 -0
  11. sembl_stack/adapters/merge_git.py +107 -0
  12. sembl_stack/adapters/postdeploy_http.py +82 -0
  13. sembl_stack/adapters/review_coderabbit.py +215 -0
  14. sembl_stack/adapters/review_llm.py +142 -0
  15. sembl_stack/adapters/review_mock.py +42 -0
  16. sembl_stack/adapters/sandbox_worktree.py +79 -0
  17. sembl_stack/adapters/spec_sembl.py +91 -0
  18. sembl_stack/adapters/verify_sembl.py +77 -0
  19. sembl_stack/artifacts.py +207 -0
  20. sembl_stack/cli.py +759 -0
  21. sembl_stack/config.py +87 -0
  22. sembl_stack/contextgraph.py +154 -0
  23. sembl_stack/doctor.py +111 -0
  24. sembl_stack/loop.py +380 -0
  25. sembl_stack/onboarding.py +272 -0
  26. sembl_stack/presets.py +114 -0
  27. sembl_stack/profile.py +193 -0
  28. sembl_stack/reconciliation.py +138 -0
  29. sembl_stack/registry.py +91 -0
  30. sembl_stack/rsi.py +188 -0
  31. sembl_stack/runner.py +134 -0
  32. sembl_stack/session.py +86 -0
  33. sembl_stack/specgraph.py +146 -0
  34. sembl_stack/store.py +112 -0
  35. sembl_stack/tracing.py +51 -0
  36. sembl_stack/transport/__init__.py +0 -0
  37. sembl_stack/transport/mcp_client.py +58 -0
  38. sembl_stack/tui.py +86 -0
  39. sembl_stack/views.py +74 -0
  40. sembl_stack/wizard.py +233 -0
  41. sembl_stack-0.1.0.dist-info/METADATA +165 -0
  42. sembl_stack-0.1.0.dist-info/RECORD +45 -0
  43. sembl_stack-0.1.0.dist-info/WHEEL +4 -0
  44. sembl_stack-0.1.0.dist-info/entry_points.txt +2 -0
  45. sembl_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,207 @@
1
+ """The artifact contract — the substrate the whole platform stands on.
2
+
3
+ Artifacts are the *only* thing stages agree on. They are plain dataclasses that are
4
+ JSON-serializable and round-trippable, so a run can be inspected, resumed, or entered
5
+ at any stage by supplying the right artifact. (See docs/PLATFORM-MAP.md §2.)
6
+
7
+ Stages transform artifacts: `inputs (typed artifacts) -> output (typed artifact)`.
8
+ Enter anywhere you can supply the inputs; exit anywhere you want the output.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import asdict, dataclass, field
14
+
15
+ ARTIFACT_VERSION = 1
16
+
17
+
18
+ class _Serializable:
19
+ """Mixin: JSON round-trip for any dataclass artifact. `KIND` tags the payload."""
20
+ KIND = "artifact"
21
+
22
+ def to_dict(self) -> dict:
23
+ d = asdict(self) # dataclass fields only; KIND is a class attr
24
+ d["_kind"] = self.KIND
25
+ d["_v"] = ARTIFACT_VERSION
26
+ return d
27
+
28
+ @classmethod
29
+ def from_dict(cls, d: dict):
30
+ fields = {k: v for k, v in d.items() if not k.startswith("_")}
31
+ return cls(**fields)
32
+
33
+ def to_json(self) -> str:
34
+ return json.dumps(self.to_dict(), indent=2, ensure_ascii=False)
35
+
36
+ @classmethod
37
+ def from_json(cls, s: str):
38
+ return cls.from_dict(json.loads(s))
39
+
40
+
41
+ # --- Artifacts ---------------------------------------------------------------
42
+
43
+ @dataclass
44
+ class Task(_Serializable):
45
+ """What the user wants. `repo` is the target working copy."""
46
+ KIND = "task"
47
+ text: str
48
+ repo: str
49
+ spec_path: str | None = None
50
+
51
+
52
+ @dataclass
53
+ class Context(_Serializable):
54
+ """Repo intelligence + pulled knowledge (L1/Brain). Optional in the short loop."""
55
+ KIND = "context"
56
+ summary: str = ""
57
+ files: list[str] = field(default_factory=list)
58
+ data: dict = field(default_factory=dict)
59
+
60
+
61
+ @dataclass
62
+ class SpecGraph(_Serializable):
63
+ """Graph form of the spec for advisory spec/code reconciliation."""
64
+ KIND = "specgraph"
65
+ nodes: list[dict] = field(default_factory=list)
66
+ edges: list[dict] = field(default_factory=list)
67
+ sources: list[str] = field(default_factory=list)
68
+ data: dict = field(default_factory=dict)
69
+
70
+
71
+ @dataclass
72
+ class Bounds(_Serializable):
73
+ """The governed scope of a change — the four-field contract Sembl verifies."""
74
+ KIND = "bounds"
75
+ editable_paths: list[str] = field(default_factory=list)
76
+ forbidden_areas: list[str] = field(default_factory=list)
77
+ churn_budget: dict = field(default_factory=dict)
78
+ sources: list[str] = field(default_factory=list)
79
+
80
+ def to_contract(self) -> dict:
81
+ """The shape `sembl verify --wo-file` consumes (no metadata)."""
82
+ return {
83
+ "editable_paths": self.editable_paths,
84
+ "forbidden_areas": self.forbidden_areas,
85
+ "churn_budget": self.churn_budget,
86
+ }
87
+
88
+
89
+ @dataclass
90
+ class Change(_Serializable):
91
+ """What the executor produced, in the sandbox: a diff + its (untrusted) report."""
92
+ KIND = "change"
93
+ diff: str
94
+ report: dict = field(default_factory=dict)
95
+ workdir: str = ""
96
+
97
+
98
+ @dataclass
99
+ class Verdict(_Serializable):
100
+ """The gate's deterministic answer."""
101
+ KIND = "verdict"
102
+ status: str = "BLOCK" # PASS | WARN | BLOCK
103
+ reasons: list[str] = field(default_factory=list)
104
+ raw: dict = field(default_factory=dict)
105
+
106
+ @property
107
+ def blocked(self) -> bool:
108
+ return self.status == "BLOCK"
109
+
110
+ def feedback(self) -> str:
111
+ """A nudge the executor can act on, on retry."""
112
+ if not self.reasons:
113
+ return ""
114
+ return ("Your previous attempt was blocked. Fix these and stay in scope:\n- "
115
+ + "\n- ".join(self.reasons))
116
+
117
+
118
+ @dataclass
119
+ class ReconciliationReport(_Serializable):
120
+ """Advisory spec/code drift report; never a gate verdict."""
121
+ KIND = "reconciliation_report"
122
+ status: str = "UNKNOWN"
123
+ summary: str = ""
124
+ findings: list[dict] = field(default_factory=list)
125
+ data: dict = field(default_factory=dict)
126
+
127
+
128
+ @dataclass
129
+ class ReviewReport(_Serializable):
130
+ """Advisory code-quality review signal (L5.5). Never a gate verdict."""
131
+ KIND = "review_report"
132
+ reviewer: str = ""
133
+ status: str = "UNKNOWN" # CLEAN | FINDINGS | UNKNOWN
134
+ findings: list[dict] = field(default_factory=list) # {severity, kind, file, message}
135
+ data: dict = field(default_factory=dict)
136
+
137
+
138
+ @dataclass
139
+ class Trace(_Serializable):
140
+ """Observability: the ordered steps of a run (L6)."""
141
+ KIND = "trace"
142
+ steps: list[dict] = field(default_factory=list)
143
+
144
+
145
+ @dataclass
146
+ class Delivery(_Serializable):
147
+ """Deploy record (Plane B / L7-L8). Defined now, used in Phase 2+."""
148
+ KIND = "delivery"
149
+ target: str = ""
150
+ url: str | None = None
151
+ status: str = "pending"
152
+ data: dict = field(default_factory=dict)
153
+
154
+
155
+ @dataclass
156
+ class MergeRecord(_Serializable):
157
+ """Gated-merge record (L6.5). PASS/WARN -> merged; BLOCK -> held."""
158
+ KIND = "merge_record"
159
+ target_branch: str = ""
160
+ source_ref: str = ""
161
+ commit: str | None = None # merge/HEAD sha when status == "merged"
162
+ status: str = "pending" # merged | held | failed
163
+ data: dict = field(default_factory=dict)
164
+
165
+
166
+ def diff_sha256(diff: str) -> str:
167
+ """The content hash that binds a Verdict to the exact diff it judged."""
168
+ import hashlib
169
+ return hashlib.sha256((diff or "").encode("utf-8")).hexdigest()
170
+
171
+
172
+ def bind_verdict(verdict: Verdict, diff: str) -> Verdict:
173
+ """Bind a Verdict to the change it judged (deep-audit item 1).
174
+
175
+ Without this, `merge`/`apply` accept ANY PASS verdict file — a verdict issued
176
+ for one change could green-light merging another. `subject` records the judged
177
+ diff's hash + file set so merge/apply can verify they act on the same change.
178
+ Mutates and returns the same Verdict."""
179
+ files = verdict.raw.get("changed_files")
180
+ if not isinstance(files, list):
181
+ try:
182
+ from sembl.validator import parse_unified_diff
183
+ files = parse_unified_diff(diff)[0]
184
+ except Exception:
185
+ files = []
186
+ verdict.raw["subject"] = {
187
+ "diff_sha256": diff_sha256(diff),
188
+ "files": sorted(files),
189
+ }
190
+ return verdict
191
+
192
+
193
+ # ExecutionResult is the legacy name for Change; kept so existing adapters import cleanly.
194
+ ExecutionResult = Change
195
+
196
+ KINDS = {c.KIND: c for c in (
197
+ Task, Context, SpecGraph, Bounds, Change, Verdict, ReconciliationReport,
198
+ ReviewReport, Trace, Delivery, MergeRecord)}
199
+
200
+
201
+ def from_dict(d: dict):
202
+ """Reconstruct the right artifact from a tagged dict (uses `_kind`)."""
203
+ kind = d.get("_kind")
204
+ cls = KINDS.get(kind)
205
+ if cls is None:
206
+ raise ValueError(f"unknown artifact kind: {kind!r}")
207
+ return cls.from_dict(d)