substratum-cli 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.
- substratum_cli/README.md +51 -0
- substratum_cli/__init__.py +6 -0
- substratum_cli/__main__.py +7 -0
- substratum_cli/_vendor/__init__.py +0 -0
- substratum_cli/_vendor/_pro_test_derive.py +169 -0
- substratum_cli/account.py +62 -0
- substratum_cli/cli.py +185 -0
- substratum_cli/codes.py +114 -0
- substratum_cli/commands/__init__.py +0 -0
- substratum_cli/commands/apply.py +49 -0
- substratum_cli/commands/auth.py +37 -0
- substratum_cli/commands/commit.py +40 -0
- substratum_cli/commands/explain.py +63 -0
- substratum_cli/commands/failing.py +80 -0
- substratum_cli/commands/fix.py +48 -0
- substratum_cli/commands/history.py +15 -0
- substratum_cli/commands/init_cmd.py +155 -0
- substratum_cli/commands/replay.py +68 -0
- substratum_cli/commands/scaffold.py +71 -0
- substratum_cli/commands/show.py +18 -0
- substratum_cli/commands/undo.py +28 -0
- substratum_cli/commands/verify.py +193 -0
- substratum_cli/commands/write.py +42 -0
- substratum_cli/config.py +49 -0
- substratum_cli/diffs.py +106 -0
- substratum_cli/engine.py +154 -0
- substratum_cli/gitio.py +102 -0
- substratum_cli/langscan.py +110 -0
- substratum_cli/product_ops.py +639 -0
- substratum_cli/refusals.py +127 -0
- substratum_cli/remote.py +140 -0
- substratum_cli/render.py +332 -0
- substratum_cli/result.py +185 -0
- substratum_cli/run_store.py +162 -0
- substratum_cli/runid.py +69 -0
- substratum_cli/session.py +558 -0
- substratum_cli/style.py +128 -0
- substratum_cli-0.1.0.dist-info/METADATA +69 -0
- substratum_cli-0.1.0.dist-info/RECORD +42 -0
- substratum_cli-0.1.0.dist-info/WHEEL +5 -0
- substratum_cli-0.1.0.dist-info/entry_points.txt +2 -0
- substratum_cli-0.1.0.dist-info/top_level.txt +1 -0
substratum_cli/result.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""SubstratumResult -- the ONE object model every command produces and every surface renders.
|
|
2
|
+
|
|
3
|
+
Stdlib-only frozen dataclasses (never pydantic; the core must import with zero web deps so
|
|
4
|
+
the CLI stays a static-binary candidate). `to_json_dict()` is the ONLY serializer: its output
|
|
5
|
+
is `--json`, is written to the run store, and is what the VS Code extension consumes. There is
|
|
6
|
+
no second serializer anywhere -- one object, many renderers, no forks.
|
|
7
|
+
|
|
8
|
+
VERIFIED and REFUSED are equally first-class shapes: a refusal carries as much structured
|
|
9
|
+
value (verified-before-refusing, named code, executable flip) as a verified fix carries
|
|
10
|
+
(diff, provenance, gate transcript), because at thin coverage the refusal is the common case.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from . import codes
|
|
20
|
+
|
|
21
|
+
SCHEMA_VERSION = "1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class NextCommand:
|
|
26
|
+
"""An executable next step. `command` is copy-pasteable; renderers may offer a "Run" button."""
|
|
27
|
+
action: str # closed verbs: scaffold_test | rerun_fix | provide_test_command | repro | show
|
|
28
|
+
target: str
|
|
29
|
+
command: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class FlipCondition:
|
|
34
|
+
"""How a refusal converts to a verified result, and WHO owns the next move.
|
|
35
|
+
owner=YOUR-TESTS -> the developer's actionable debt (add a test pinning X).
|
|
36
|
+
owner=SUBSTRATUM-COVERAGE -> our gap; never billed as their debt."""
|
|
37
|
+
owner: str # "YOUR-TESTS" | "SUBSTRATUM-COVERAGE"
|
|
38
|
+
steps: Tuple[NextCommand, ...] = ()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Refusal:
|
|
43
|
+
"""The five mandatory elements. A refusal missing any of these is a product bug (CI enforces)."""
|
|
44
|
+
code: str # a codes.REFUSAL_CODES member
|
|
45
|
+
engine_code: str # the raw engine/gate code, preserved for audit
|
|
46
|
+
why: str
|
|
47
|
+
verified_before_refusing: Tuple[str, ...] # e.g. ("reproduced: test FAILS at base", "composed: 6 candidates, all gated out")
|
|
48
|
+
flip: FlipCondition
|
|
49
|
+
permanence: str = "Retrying without changing inputs returns this exact refusal."
|
|
50
|
+
|
|
51
|
+
def is_complete(self) -> bool:
|
|
52
|
+
return bool(self.code and self.why and self.flip.owner
|
|
53
|
+
and self.flip.steps and self.permanence)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class GateTranscript:
|
|
58
|
+
kind: str # "client_differential" | "docker" | "scaffold_check"
|
|
59
|
+
test_ids: Tuple[str, ...] = ()
|
|
60
|
+
runner_command: Tuple[str, ...] = ()
|
|
61
|
+
base_status: str = "" # "FAIL" (required for differential) | "PASS" | "ERROR" | ...
|
|
62
|
+
patched_status: str = ""
|
|
63
|
+
base_excerpt: str = ""
|
|
64
|
+
patched_excerpt: str = ""
|
|
65
|
+
worktree_isolated: bool = True
|
|
66
|
+
impacted: Optional[dict] = None # {"tests_run": N, "regressions": 0} or None until run
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Provenance:
|
|
71
|
+
entries: Tuple[str, ...] = () # SubstrateCandidate.provenance verbatim
|
|
72
|
+
pattern_id: Optional[str] = None
|
|
73
|
+
corpus_support: Optional[int] = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class HunkVerdict:
|
|
78
|
+
path: str
|
|
79
|
+
hunk_index: int
|
|
80
|
+
header: str
|
|
81
|
+
verdict: str
|
|
82
|
+
code: Optional[str] = None
|
|
83
|
+
next_command: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class EngineInfo:
|
|
88
|
+
engine_version: str = ""
|
|
89
|
+
library_versions: Tuple[str, ...] = ()
|
|
90
|
+
egress: str = "none" # checked, not claimed (doctor verifies)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class SubstratumResult:
|
|
95
|
+
verdict: str # a codes.VERDICTS member
|
|
96
|
+
run_id: str
|
|
97
|
+
operation: str # fix | verify | init | scaffold_test | coverage_map | explain | replay | show | apply | doctor
|
|
98
|
+
code: Optional[str] = None
|
|
99
|
+
summary: str = "" # the first line, "<VERDICT> run=<id> [code=<CODE>]"
|
|
100
|
+
diff: str = ""
|
|
101
|
+
files: Tuple[str, ...] = ()
|
|
102
|
+
provenance: Optional[Provenance] = None
|
|
103
|
+
gate: Optional[GateTranscript] = None
|
|
104
|
+
refusal: Optional[Refusal] = None
|
|
105
|
+
hunks: Tuple[HunkVerdict, ...] = ()
|
|
106
|
+
engine: EngineInfo = field(default_factory=EngineInfo)
|
|
107
|
+
data: Optional[dict] = None # command-specific payload (coverage_map/explain/replay)
|
|
108
|
+
schema_version: str = SCHEMA_VERSION
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def exit_code(self) -> int:
|
|
112
|
+
return codes.exit_code_for(self.verdict, self.code)
|
|
113
|
+
|
|
114
|
+
def to_json_dict(self) -> dict:
|
|
115
|
+
"""The ONE serializer. Tuples -> lists; exit_code emitted in-band (so a non-process
|
|
116
|
+
consumer -- VS Code -- branches without re-deriving it). Stable key order via asdict."""
|
|
117
|
+
d = dataclasses.asdict(self)
|
|
118
|
+
d["exit_code"] = self.exit_code
|
|
119
|
+
return d
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# constructors + the first-line grammar (shared by every surface)
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def verdict_line(verdict: str, run_id: str, code: Optional[str] = None) -> str:
|
|
127
|
+
"""The machine-parseable first line, identical everywhere:
|
|
128
|
+
<VERDICT> run=<id> [code=<CODE>]
|
|
129
|
+
Two spaces between fields (the grammar UX_SCOPE pins)."""
|
|
130
|
+
line = f"{verdict} run={run_id}"
|
|
131
|
+
if code:
|
|
132
|
+
line += f" code={code}"
|
|
133
|
+
return line
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def make(verdict: str, run_id: str, operation: str, **kw) -> SubstratumResult:
|
|
137
|
+
"""Build a result with its summary line filled in from verdict/run_id/code."""
|
|
138
|
+
code = kw.get("code")
|
|
139
|
+
summary = verdict_line(verdict, run_id, code)
|
|
140
|
+
return SubstratumResult(verdict=verdict, run_id=run_id, operation=operation,
|
|
141
|
+
summary=summary, **kw)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def from_json_dict(d: dict) -> SubstratumResult:
|
|
145
|
+
"""Rehydrate a stored/received result. Tolerant of the extra in-band exit_code key."""
|
|
146
|
+
d = dict(d)
|
|
147
|
+
d.pop("exit_code", None)
|
|
148
|
+
prov = d.get("provenance")
|
|
149
|
+
gate = d.get("gate")
|
|
150
|
+
refusal = d.get("refusal")
|
|
151
|
+
eng = d.get("engine") or {}
|
|
152
|
+
hunks = d.get("hunks") or ()
|
|
153
|
+
kw = dict(
|
|
154
|
+
verdict=d["verdict"], run_id=d["run_id"], operation=d["operation"],
|
|
155
|
+
code=d.get("code"), summary=d.get("summary", ""), diff=d.get("diff", ""),
|
|
156
|
+
files=tuple(d.get("files", ())),
|
|
157
|
+
provenance=(Provenance(entries=tuple(prov.get("entries", ())),
|
|
158
|
+
pattern_id=prov.get("pattern_id"),
|
|
159
|
+
corpus_support=prov.get("corpus_support")) if prov else None),
|
|
160
|
+
gate=(GateTranscript(**{**gate, "test_ids": tuple(gate.get("test_ids", ())),
|
|
161
|
+
"runner_command": tuple(gate.get("runner_command", ()))})
|
|
162
|
+
if gate else None),
|
|
163
|
+
refusal=(_refusal_from(refusal) if refusal else None),
|
|
164
|
+
hunks=tuple(HunkVerdict(**h) for h in hunks),
|
|
165
|
+
engine=EngineInfo(engine_version=eng.get("engine_version", ""),
|
|
166
|
+
library_versions=tuple(eng.get("library_versions", ())),
|
|
167
|
+
egress=eng.get("egress", "none")),
|
|
168
|
+
data=d.get("data"), schema_version=d.get("schema_version", SCHEMA_VERSION),
|
|
169
|
+
)
|
|
170
|
+
return SubstratumResult(**kw)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _refusal_from(r: dict) -> Refusal:
|
|
174
|
+
flip = r.get("flip") or {}
|
|
175
|
+
steps = tuple(NextCommand(**s) for s in flip.get("steps", ()))
|
|
176
|
+
return Refusal(
|
|
177
|
+
code=r["code"], engine_code=r.get("engine_code", ""), why=r.get("why", ""),
|
|
178
|
+
verified_before_refusing=tuple(r.get("verified_before_refusing", ())),
|
|
179
|
+
flip=FlipCondition(owner=flip.get("owner", ""), steps=steps),
|
|
180
|
+
permanence=r.get("permanence", ""),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def dumps(r: SubstratumResult) -> str:
|
|
185
|
+
return json.dumps(r.to_json_dict(), indent=2, sort_keys=False)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""The local proof store: `.substratum/` at the repo root. Every run -- VERIFIED or REFUSED --
|
|
2
|
+
is persisted so it is `substratum show`-able, replayable, and attestable, and so refusals feed
|
|
3
|
+
the conversion ledger. Atomic writes (tmp + rename). No network, no object-DB writes."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from . import result as R
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def store_dir(repo: str) -> str:
|
|
15
|
+
return os.path.join(repo, ".substratum")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def runs_dir(repo: str) -> str:
|
|
19
|
+
return os.path.join(store_dir(repo), "runs")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_dir(repo: str, run_id: str) -> str:
|
|
23
|
+
return os.path.join(runs_dir(repo), run_id)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _atomic_write(path: str, text: str) -> None:
|
|
27
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
28
|
+
tmp = f"{path}.tmp{os.getpid()}"
|
|
29
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
30
|
+
fh.write(text)
|
|
31
|
+
os.replace(tmp, path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def store_run(repo: str, res: "R.SubstratumResult", run_key: Optional[dict] = None,
|
|
35
|
+
inputs: Optional[dict] = None) -> str:
|
|
36
|
+
"""Write record.json (canonical) + diff.patch + inputs.json (for replay). Returns the dir."""
|
|
37
|
+
d = run_dir(repo, res.run_id)
|
|
38
|
+
_atomic_write(os.path.join(d, "record.json"), R.dumps(res))
|
|
39
|
+
if res.diff:
|
|
40
|
+
_atomic_write(os.path.join(d, "diff.patch"), res.diff)
|
|
41
|
+
if run_key is not None or inputs is not None:
|
|
42
|
+
_atomic_write(os.path.join(d, "inputs.json"),
|
|
43
|
+
json.dumps({"run_key": run_key, "inputs": inputs or {}}, indent=2))
|
|
44
|
+
if res.gate:
|
|
45
|
+
if res.gate.base_excerpt:
|
|
46
|
+
_atomic_write(os.path.join(d, "gate", "base.log"), res.gate.base_excerpt)
|
|
47
|
+
if res.gate.patched_excerpt:
|
|
48
|
+
_atomic_write(os.path.join(d, "gate", "patched.log"), res.gate.patched_excerpt)
|
|
49
|
+
if res.verdict in ("REFUSED", "UNVERIFIABLE") and res.refusal:
|
|
50
|
+
_append_refusal(repo, res)
|
|
51
|
+
elif res.verdict in ("VERIFIED", "PASS"):
|
|
52
|
+
_mark_converted(repo, res)
|
|
53
|
+
return d
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _target_of_record(j: dict) -> str:
|
|
57
|
+
data = j.get("data") or {}
|
|
58
|
+
if data.get("target"):
|
|
59
|
+
return str(data["target"])
|
|
60
|
+
gate = j.get("gate") or {}
|
|
61
|
+
if gate.get("test_ids"):
|
|
62
|
+
return ", ".join(gate["test_ids"][:2])
|
|
63
|
+
if j.get("files"):
|
|
64
|
+
return ", ".join(j["files"][:2])
|
|
65
|
+
return j.get("operation", "")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_runs(repo: str, limit: int = 20) -> list:
|
|
69
|
+
"""Newest-first run history: [{run_id, verdict, operation, code, target, ts}]. The proof store,
|
|
70
|
+
made visible -- every VERIFIED and every REFUSED, so the developer sees the whole trail."""
|
|
71
|
+
d = runs_dir(repo)
|
|
72
|
+
if not os.path.isdir(d):
|
|
73
|
+
return []
|
|
74
|
+
out = []
|
|
75
|
+
for rid in os.listdir(d):
|
|
76
|
+
rec = os.path.join(d, rid, "record.json")
|
|
77
|
+
if not os.path.isfile(rec):
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
j = json.load(open(rec))
|
|
81
|
+
except Exception:
|
|
82
|
+
continue
|
|
83
|
+
out.append({"run_id": j.get("run_id", rid), "verdict": j.get("verdict", ""),
|
|
84
|
+
"operation": j.get("operation", ""), "code": j.get("code"),
|
|
85
|
+
"target": _target_of_record(j), "ts": os.path.getmtime(rec)})
|
|
86
|
+
out.sort(key=lambda e: e["ts"], reverse=True)
|
|
87
|
+
return out[:limit]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_ids(repo: str) -> list:
|
|
91
|
+
"""All stored run ids (for tab-completion)."""
|
|
92
|
+
d = runs_dir(repo)
|
|
93
|
+
if not os.path.isdir(d):
|
|
94
|
+
return []
|
|
95
|
+
return [r for r in os.listdir(d) if os.path.isfile(os.path.join(d, r, "record.json"))]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_run(repo: str, run_id: str) -> Optional[tuple["R.SubstratumResult", dict]]:
|
|
99
|
+
d = run_dir(repo, run_id)
|
|
100
|
+
rec = os.path.join(d, "record.json")
|
|
101
|
+
if not os.path.exists(rec):
|
|
102
|
+
return None
|
|
103
|
+
res = R.from_json_dict(json.load(open(rec)))
|
|
104
|
+
inp = {}
|
|
105
|
+
ip = os.path.join(d, "inputs.json")
|
|
106
|
+
if os.path.exists(ip):
|
|
107
|
+
inp = json.load(open(ip))
|
|
108
|
+
return res, inp
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _refusals_path(repo: str) -> str:
|
|
112
|
+
return os.path.join(store_dir(repo), "refusals.jsonl")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _target_of(res: "R.SubstratumResult") -> str:
|
|
116
|
+
"""A stable target key so a later VERIFIED can convert an earlier REFUSED: the sorted
|
|
117
|
+
failing-test ids or the touched files."""
|
|
118
|
+
q = (res.data or {}).get("target") if res.data else None
|
|
119
|
+
if q:
|
|
120
|
+
return str(q)
|
|
121
|
+
if res.gate and res.gate.test_ids:
|
|
122
|
+
return "|".join(sorted(res.gate.test_ids))
|
|
123
|
+
if res.files:
|
|
124
|
+
return "|".join(sorted(res.files))
|
|
125
|
+
return res.operation
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _append_refusal(repo: str, res: "R.SubstratumResult") -> None:
|
|
129
|
+
path = _refusals_path(repo)
|
|
130
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
131
|
+
rec = {"run_id": res.run_id, "code": res.code, "target": _target_of(res),
|
|
132
|
+
"flip_owner": res.refusal.flip.owner if res.refusal else "",
|
|
133
|
+
"converted_to": None, "_ts": int(time.time())}
|
|
134
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
135
|
+
fh.write(json.dumps(rec) + "\n")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _mark_converted(repo: str, res: "R.SubstratumResult") -> None:
|
|
139
|
+
"""A VERIFIED on a target back-fills any unconverted refusal on the same target."""
|
|
140
|
+
path = _refusals_path(repo)
|
|
141
|
+
if not os.path.exists(path):
|
|
142
|
+
return
|
|
143
|
+
target = _target_of(res)
|
|
144
|
+
lines = [json.loads(l) for l in open(path) if l.strip()]
|
|
145
|
+
changed = False
|
|
146
|
+
for rec in lines:
|
|
147
|
+
if rec.get("target") == target and rec.get("converted_to") is None:
|
|
148
|
+
rec["converted_to"] = res.run_id
|
|
149
|
+
changed = True
|
|
150
|
+
if changed:
|
|
151
|
+
_atomic_write(path, "".join(json.dumps(r) + "\n" for r in lines))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def refusal_stats(repo: str, window_days: int = 7) -> tuple[int, int]:
|
|
155
|
+
"""(refusals in window, of which converted). The go/no-go health metric."""
|
|
156
|
+
path = _refusals_path(repo)
|
|
157
|
+
if not os.path.exists(path):
|
|
158
|
+
return (0, 0)
|
|
159
|
+
cutoff = int(time.time()) - window_days * 86400
|
|
160
|
+
recs = [json.loads(l) for l in open(path) if l.strip()]
|
|
161
|
+
recent = [r for r in recs if r.get("_ts", 0) >= cutoff]
|
|
162
|
+
return (len(recent), sum(1 for r in recent if r.get("converted_to")))
|
substratum_cli/runid.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Content-hash run IDs. The determinism story made shared: same repo state + same query +
|
|
2
|
+
same engine/library versions -> the same `sub_...` id, for two engineers or an agent and its
|
|
3
|
+
human. Computed here and ONLY here, so no surface forks the id.
|
|
4
|
+
|
|
5
|
+
Nothing in the RunKey is volatile (no timestamps, no worktree paths), so a re-run of the same
|
|
6
|
+
inputs re-derives the same id -- which is exactly what `replay` asserts.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from . import gitio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def canonical(obj: dict) -> str:
|
|
18
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def library_versions(repo_root: str = "/home/thepadrevictor/Substratum") -> dict:
|
|
22
|
+
"""{lang: '<library-root-basename>@<manifest-hash>'} for each active root, or 'absent'.
|
|
23
|
+
Loaded via the leaf bridge (lib_roots is import-clean) so no numpy chain is paid."""
|
|
24
|
+
import os
|
|
25
|
+
out = {}
|
|
26
|
+
try:
|
|
27
|
+
from .engine import lib_roots
|
|
28
|
+
getters = {"py": lib_roots.py_library_root, "go": lib_roots.go_library_root,
|
|
29
|
+
"ts": lib_roots.ts_library_root}
|
|
30
|
+
for lang, get in getters.items():
|
|
31
|
+
root = get()
|
|
32
|
+
if not root or not os.path.isdir(root):
|
|
33
|
+
out[lang] = "absent"
|
|
34
|
+
continue
|
|
35
|
+
man = os.path.join(root, "manifest.json")
|
|
36
|
+
h = ""
|
|
37
|
+
if os.path.exists(man):
|
|
38
|
+
h = hashlib.sha256(open(man, "rb").read()).hexdigest()[:12]
|
|
39
|
+
out[lang] = f"{os.path.basename(root)}@{h}"
|
|
40
|
+
except Exception:
|
|
41
|
+
out = {"py": "unknown", "go": "unknown", "ts": "unknown"}
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def engine_version(repo_root: str = "/home/thepadrevictor/Substratum") -> str:
|
|
46
|
+
"""The engine's git sha (dev fallback). A stamped build overrides via SUBSTRATUM_ENGINE_SHA."""
|
|
47
|
+
import os
|
|
48
|
+
env = os.environ.get("SUBSTRATUM_ENGINE_SHA")
|
|
49
|
+
if env:
|
|
50
|
+
return env
|
|
51
|
+
return gitio.head_sha(repo_root) or "dev"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def compute(repo: str, operation: str, query: dict,
|
|
55
|
+
engine_sha: Optional[str] = None,
|
|
56
|
+
libs: Optional[dict] = None,
|
|
57
|
+
pkg_version: str = "0.1.0") -> tuple[str, dict]:
|
|
58
|
+
"""Return (run_id, run_key). run_key is stored verbatim so `replay` can prove state identity."""
|
|
59
|
+
key = {
|
|
60
|
+
"head_sha": gitio.head_sha(repo) if gitio.is_git_repo(repo) else "",
|
|
61
|
+
"dirty_hash": gitio.dirty_hash(repo) if gitio.is_git_repo(repo) else "",
|
|
62
|
+
"operation": operation,
|
|
63
|
+
"query": query,
|
|
64
|
+
"engine_sha": engine_sha if engine_sha is not None else engine_version(),
|
|
65
|
+
"pkg_version": pkg_version,
|
|
66
|
+
"library_versions": libs if libs is not None else library_versions(),
|
|
67
|
+
}
|
|
68
|
+
digest = hashlib.sha256(canonical(key).encode()).hexdigest()[:12]
|
|
69
|
+
return f"sub_{digest}", key
|