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.
- sembl_stack/__init__.py +3 -0
- sembl_stack/adapters/__init__.py +0 -0
- sembl_stack/adapters/_redact.py +19 -0
- sembl_stack/adapters/base.py +179 -0
- sembl_stack/adapters/codegraph_cbm.py +95 -0
- sembl_stack/adapters/deploy_vercel.py +215 -0
- sembl_stack/adapters/execute_aider.py +115 -0
- sembl_stack/adapters/execute_claude.py +114 -0
- sembl_stack/adapters/execute_mock.py +53 -0
- sembl_stack/adapters/execute_opencode.py +114 -0
- sembl_stack/adapters/merge_git.py +107 -0
- sembl_stack/adapters/postdeploy_http.py +82 -0
- sembl_stack/adapters/review_coderabbit.py +215 -0
- sembl_stack/adapters/review_llm.py +142 -0
- sembl_stack/adapters/review_mock.py +42 -0
- sembl_stack/adapters/sandbox_worktree.py +79 -0
- sembl_stack/adapters/spec_sembl.py +91 -0
- sembl_stack/adapters/verify_sembl.py +77 -0
- sembl_stack/artifacts.py +207 -0
- sembl_stack/cli.py +759 -0
- sembl_stack/config.py +87 -0
- sembl_stack/contextgraph.py +154 -0
- sembl_stack/doctor.py +111 -0
- sembl_stack/loop.py +380 -0
- sembl_stack/onboarding.py +272 -0
- sembl_stack/presets.py +114 -0
- sembl_stack/profile.py +193 -0
- sembl_stack/reconciliation.py +138 -0
- sembl_stack/registry.py +91 -0
- sembl_stack/rsi.py +188 -0
- sembl_stack/runner.py +134 -0
- sembl_stack/session.py +86 -0
- sembl_stack/specgraph.py +146 -0
- sembl_stack/store.py +112 -0
- sembl_stack/tracing.py +51 -0
- sembl_stack/transport/__init__.py +0 -0
- sembl_stack/transport/mcp_client.py +58 -0
- sembl_stack/tui.py +86 -0
- sembl_stack/views.py +74 -0
- sembl_stack/wizard.py +233 -0
- sembl_stack-0.1.0.dist-info/METADATA +165 -0
- sembl_stack-0.1.0.dist-info/RECORD +45 -0
- sembl_stack-0.1.0.dist-info/WHEEL +4 -0
- sembl_stack-0.1.0.dist-info/entry_points.txt +2 -0
- sembl_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
sembl_stack/artifacts.py
ADDED
|
@@ -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)
|