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.
Files changed (42) hide show
  1. substratum_cli/README.md +51 -0
  2. substratum_cli/__init__.py +6 -0
  3. substratum_cli/__main__.py +7 -0
  4. substratum_cli/_vendor/__init__.py +0 -0
  5. substratum_cli/_vendor/_pro_test_derive.py +169 -0
  6. substratum_cli/account.py +62 -0
  7. substratum_cli/cli.py +185 -0
  8. substratum_cli/codes.py +114 -0
  9. substratum_cli/commands/__init__.py +0 -0
  10. substratum_cli/commands/apply.py +49 -0
  11. substratum_cli/commands/auth.py +37 -0
  12. substratum_cli/commands/commit.py +40 -0
  13. substratum_cli/commands/explain.py +63 -0
  14. substratum_cli/commands/failing.py +80 -0
  15. substratum_cli/commands/fix.py +48 -0
  16. substratum_cli/commands/history.py +15 -0
  17. substratum_cli/commands/init_cmd.py +155 -0
  18. substratum_cli/commands/replay.py +68 -0
  19. substratum_cli/commands/scaffold.py +71 -0
  20. substratum_cli/commands/show.py +18 -0
  21. substratum_cli/commands/undo.py +28 -0
  22. substratum_cli/commands/verify.py +193 -0
  23. substratum_cli/commands/write.py +42 -0
  24. substratum_cli/config.py +49 -0
  25. substratum_cli/diffs.py +106 -0
  26. substratum_cli/engine.py +154 -0
  27. substratum_cli/gitio.py +102 -0
  28. substratum_cli/langscan.py +110 -0
  29. substratum_cli/product_ops.py +639 -0
  30. substratum_cli/refusals.py +127 -0
  31. substratum_cli/remote.py +140 -0
  32. substratum_cli/render.py +332 -0
  33. substratum_cli/result.py +185 -0
  34. substratum_cli/run_store.py +162 -0
  35. substratum_cli/runid.py +69 -0
  36. substratum_cli/session.py +558 -0
  37. substratum_cli/style.py +128 -0
  38. substratum_cli-0.1.0.dist-info/METADATA +69 -0
  39. substratum_cli-0.1.0.dist-info/RECORD +42 -0
  40. substratum_cli-0.1.0.dist-info/WHEEL +5 -0
  41. substratum_cli-0.1.0.dist-info/entry_points.txt +2 -0
  42. substratum_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -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")))
@@ -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