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/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Substratum
|
|
2
|
+
|
|
3
|
+
**Verified-or-refused code, with provenance.** Substratum is a deterministic comprehension engine
|
|
4
|
+
for code. It never streams tokens and hopes — it produces a change only when that change passes a
|
|
5
|
+
gate, and otherwise returns an honest, actionable refusal. Every result carries a run id you can
|
|
6
|
+
replay byte-for-byte.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install substratum-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then, in any repository:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
substratum # open the interactive session (warms, then drives)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
- **`summarize`** — describe the codebase in plain English, grounded in its real symbols (not a guess).
|
|
21
|
+
- **`failing`** — run the suite and list the failing tests; each becomes a ready fix target.
|
|
22
|
+
- **`verify`** — gate a diff (stdin, `--staged`, `--commit`): PASS / FAIL / per-hunk. The wedge — point
|
|
23
|
+
it at any tool's diff and it tells you, deterministically, whether the change is real.
|
|
24
|
+
- **`fix`** — produce a verified fix for a failing test, or a named refusal.
|
|
25
|
+
- **`write`** — greenfield code from plain intent (derive-or-refuse; no test required).
|
|
26
|
+
- **`runs` / `show` / `replay` / `undo` / `apply` / `commit`** — a proof-carrying memory of every run.
|
|
27
|
+
|
|
28
|
+
`summarize`, `failing`, and `verify` run **entirely on your machine** — no account, no network, no code
|
|
29
|
+
leaves your repo. They work air-gapped.
|
|
30
|
+
|
|
31
|
+
## The hosted engine (optional)
|
|
32
|
+
|
|
33
|
+
`fix` and `write` draw on a large mined library. During the demo that library is hosted:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
substratum login # GitHub device flow; connects to the hosted engine
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When logged in, `write` sends only your intent + signature, and `fix` receives a *candidate* that your
|
|
40
|
+
machine then gates locally — so your code and your tests never leave your machine. `substratum logout`
|
|
41
|
+
returns to fully-local operation.
|
|
42
|
+
|
|
43
|
+
## The law
|
|
44
|
+
|
|
45
|
+
Never render unproven code. A refusal is a deliverable, not an error. Determinism is a button you
|
|
46
|
+
press to catch us: run the same request twice and get the same run id, or `replay` any run and get a
|
|
47
|
+
byte-identical result.
|
|
48
|
+
|
|
49
|
+
Coverage grows with data. Today the engine refuses more than it resolves, by design — the surface is
|
|
50
|
+
built to feel complete at thin coverage, and the same commands silently resolve more as the library
|
|
51
|
+
scales, with zero change on your end.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""substratum_cli -- the product surface (CLI + shared object model) over the Substratum engine.
|
|
2
|
+
|
|
3
|
+
Import budget: this package is stdlib-only; the ONLY module reaching into backend.* is
|
|
4
|
+
`engine.py`, lazily. Importing substratum_cli (or any light command) opens nothing heavy.
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Derive a standalone GATED acceptance test for a single target function from a Pro test_patch.
|
|
2
|
+
|
|
3
|
+
Recipe (from the mapping): take the test_patch added lines, extract the test functions that are in
|
|
4
|
+
fail_to_pass AND call the target, keep only self-contained ones (no heavy fixtures / heavy objects),
|
|
5
|
+
rewrite MOD.fn -> fn and inject `from solution import fn`. Returns the test string or None (-> intent-alone).
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
_HEAVY_FIXTURES = {
|
|
10
|
+
"caplog", "capsys", "monkeypatch", "config_stub", "benchmark", "qtbot", "tmp_path", "tmpdir",
|
|
11
|
+
"mocker", "win_registry", "fake_web_tab", "responses", "qt_logger", "host_blocker_factory",
|
|
12
|
+
"mock_datetime", "tmpdir_factory", "request", "freezer",
|
|
13
|
+
}
|
|
14
|
+
_HEAVY_TOKENS = ("QUrl", "QtCore", "QtGui", "QtWidgets", "usertypes.", "objreg.", "asyncio.run",
|
|
15
|
+
"await ", "PyQt", "qtbot", "QApplication", "mock.", "Mock(", "MagicMock")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _added_lines(test_patch: str):
|
|
19
|
+
out = []
|
|
20
|
+
for l in test_patch.split("\n"):
|
|
21
|
+
if l.startswith("+++") or l.startswith("---") or l.startswith("@@"):
|
|
22
|
+
continue
|
|
23
|
+
if l.startswith("+"):
|
|
24
|
+
out.append(l[1:])
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ftp_names(fail_to_pass):
|
|
29
|
+
names = set()
|
|
30
|
+
for f in fail_to_pass or ():
|
|
31
|
+
leaf = f.split("::")[-1]
|
|
32
|
+
names.add(leaf.split("[")[0].strip())
|
|
33
|
+
return names
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _extract_regions(added, name):
|
|
37
|
+
"""[(fn, indent, [lines])] for each def test_* with its decorators + indented body."""
|
|
38
|
+
regions = []
|
|
39
|
+
i, n = 0, len(added)
|
|
40
|
+
while i < n:
|
|
41
|
+
m = re.match(r"(\s*)def (test_\w+)\s*\(", added[i])
|
|
42
|
+
if not m:
|
|
43
|
+
i += 1
|
|
44
|
+
continue
|
|
45
|
+
indent = len(m.group(1))
|
|
46
|
+
fn = m.group(2)
|
|
47
|
+
# decorators above: the contiguous non-blank block, from its first @-line down (captures a
|
|
48
|
+
# MULTI-LINE @pytest.mark.parametrize whose tuple list spans many lines before the def).
|
|
49
|
+
above = []
|
|
50
|
+
j = i - 1
|
|
51
|
+
while j >= 0 and added[j].strip() != "":
|
|
52
|
+
above.insert(0, added[j]); j -= 1
|
|
53
|
+
dec_start = next((idx for idx, l in enumerate(above) if l.lstrip().startswith("@")), None)
|
|
54
|
+
decs = above[dec_start:] if dec_start is not None else []
|
|
55
|
+
body = [added[i]]
|
|
56
|
+
k = i + 1
|
|
57
|
+
while k < n:
|
|
58
|
+
ln = added[k]
|
|
59
|
+
if ln.strip() == "" or (len(ln) - len(ln.lstrip())) > indent:
|
|
60
|
+
body.append(ln)
|
|
61
|
+
k += 1
|
|
62
|
+
else:
|
|
63
|
+
break
|
|
64
|
+
regions.append((fn, indent, decs + body))
|
|
65
|
+
i = k
|
|
66
|
+
return regions
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _dedent(lines, indent):
|
|
70
|
+
out = []
|
|
71
|
+
for l in lines:
|
|
72
|
+
out.append(l[indent:] if len(l) >= indent and l[:indent].strip() == "" else l.lstrip())
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def derive_gated_test(test_patch: str, name: str, fail_to_pass):
|
|
77
|
+
if not test_patch or not name:
|
|
78
|
+
return None
|
|
79
|
+
added = _added_lines(test_patch)
|
|
80
|
+
ftp = _ftp_names(fail_to_pass)
|
|
81
|
+
regions = _extract_regions(added, name)
|
|
82
|
+
call_re = re.compile(r"(\b\w+\.)?\b" + re.escape(name) + r"\s*\(")
|
|
83
|
+
kept = []
|
|
84
|
+
for fn, indent, lines in regions:
|
|
85
|
+
if ftp and fn not in ftp:
|
|
86
|
+
continue
|
|
87
|
+
region_text = "\n".join(lines)
|
|
88
|
+
if not call_re.search(region_text):
|
|
89
|
+
continue
|
|
90
|
+
# self-contained: def params carry no heavy fixture; body no heavy token
|
|
91
|
+
def_line = next((l for l in lines if l.lstrip().startswith("def ")), "")
|
|
92
|
+
params = def_line[def_line.find("(") + 1: def_line.rfind(")")] if "(" in def_line else ""
|
|
93
|
+
param_names = {p.strip().split(":")[0].split("=")[0].strip() for p in params.split(",")}
|
|
94
|
+
if param_names & _HEAVY_FIXTURES:
|
|
95
|
+
continue
|
|
96
|
+
if any(tok in region_text for tok in _HEAVY_TOKENS):
|
|
97
|
+
continue
|
|
98
|
+
# METHOD guard: if the target is invoked as QUAL.name( and QUAL is a LOCAL object constructed
|
|
99
|
+
# in the test body (QUAL = ...), it is a method needing instance state -> a free function the
|
|
100
|
+
# engine synthesizes cannot satisfy it. (utils.parse_duration: `utils` is a module top-import,
|
|
101
|
+
# never assigned -> kept. entity.get_statement_values: `entity = createWikidataEntity()` -> dropped.)
|
|
102
|
+
quals = set(re.findall(r"(\b\w+)\." + re.escape(name) + r"\s*\(", region_text))
|
|
103
|
+
if any(re.search(r"(?m)^\s*" + re.escape(q) + r"\s*=", region_text) for q in quals):
|
|
104
|
+
continue
|
|
105
|
+
kept.append((fn, indent, lines))
|
|
106
|
+
if not kept:
|
|
107
|
+
return None
|
|
108
|
+
# stdlib imports the kept regions may use (best-effort: keep added import lines that are not repo imports)
|
|
109
|
+
stdlib_imports = []
|
|
110
|
+
for l in added:
|
|
111
|
+
s = l.strip()
|
|
112
|
+
if s.startswith(("import ", "from ")) and " import " in (s + " "):
|
|
113
|
+
mod = s.split()[1].split(".")[0]
|
|
114
|
+
if mod in ("re", "math", "datetime", "logging", "dataclasses", "json", "itertools",
|
|
115
|
+
"collections", "enum", "functools", "string", "decimal", "fractions"):
|
|
116
|
+
stdlib_imports.append(s)
|
|
117
|
+
body_blocks = []
|
|
118
|
+
for fn, indent, lines in kept:
|
|
119
|
+
ded = _dedent(lines, indent)
|
|
120
|
+
text = "\n".join(ded)
|
|
121
|
+
# strip a leading `self, ` / `self` from the def (class-method -> module-level)
|
|
122
|
+
text = re.sub(r"(def test_\w+\()self,\s*", r"\1", text)
|
|
123
|
+
text = re.sub(r"(def test_\w+\()self\)", r"\1)", text)
|
|
124
|
+
# rewrite MOD.name( -> name(
|
|
125
|
+
text = re.sub(r"\b\w+\." + re.escape(name) + r"\b", name, text)
|
|
126
|
+
body_blocks.append(text)
|
|
127
|
+
# NEED-DRIVEN stdlib imports: the added lines carry an import only when the PATCH adds it,
|
|
128
|
+
# but the original test file usually imported the module already (the patch only adds the
|
|
129
|
+
# test function). Scan the kept regions for `mod.` usages of the same stdlib allowlist and
|
|
130
|
+
# import every one the carried lines missed -- else the derived snippet NameErrors at
|
|
131
|
+
# collection and the gate can never evaluate ANY candidate.
|
|
132
|
+
_STDLIB = ("re", "math", "datetime", "logging", "dataclasses", "json", "itertools",
|
|
133
|
+
"collections", "enum", "functools", "string", "decimal", "fractions")
|
|
134
|
+
all_text = "\n".join(body_blocks)
|
|
135
|
+
carried = {l.split()[1].split(".")[0] for l in stdlib_imports}
|
|
136
|
+
for mod in _STDLIB:
|
|
137
|
+
if mod not in carried and re.search(rf"\b{mod}\.", all_text):
|
|
138
|
+
stdlib_imports.append(f"import {mod}")
|
|
139
|
+
header = "import pytest\n" + ("\n".join(dict.fromkeys(stdlib_imports)) + "\n" if stdlib_imports else "")
|
|
140
|
+
header += f"from solution import {name}\n"
|
|
141
|
+
return header + "\n\n" + "\n\n".join(body_blocks) + "\n"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
import json
|
|
146
|
+
D = "/mnt/f/luca-lm-data/swe_bench/pro_test.jsonl"
|
|
147
|
+
rows = [json.loads(l) for l in open(D) if l.strip()]
|
|
148
|
+
targets = {
|
|
149
|
+
"parse_duration": "instance_qutebrowser__qutebrowser-96b997802e94",
|
|
150
|
+
"widened_hostnames": "instance_qutebrowser__qutebrowser-c580ebf0801e",
|
|
151
|
+
"get_statement_values": "instance_internetarchive__openlibrary-4a5d2a7d",
|
|
152
|
+
}
|
|
153
|
+
by_id = {r["instance_id"]: r for r in rows}
|
|
154
|
+
for name, idpfx in targets.items():
|
|
155
|
+
inst = next((r for iid, r in by_id.items() if iid.startswith(idpfx)), None)
|
|
156
|
+
if inst is None:
|
|
157
|
+
print(f"\n=== {name}: instance not found ({idpfx}) ===")
|
|
158
|
+
continue
|
|
159
|
+
ftp = inst.get("fail_to_pass")
|
|
160
|
+
if isinstance(ftp, str):
|
|
161
|
+
try:
|
|
162
|
+
ftp = json.loads(ftp)
|
|
163
|
+
except Exception:
|
|
164
|
+
ftp = [x.strip().strip("'\"") for x in ftp.strip("[]").split(",")]
|
|
165
|
+
t = derive_gated_test(inst.get("test_patch", "") or "", name, ftp or [])
|
|
166
|
+
print(f"\n{'='*70}\n=== {name} ({inst['instance_id'][:40]}) ===")
|
|
167
|
+
print("DERIVED TEST:" if t else "NO SELF-CONTAINED TEST (-> intent-alone)")
|
|
168
|
+
if t:
|
|
169
|
+
print(t[:1200])
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Global per-user account state: `~/.substratum/config.json` -- the login token, the server URL,
|
|
2
|
+
and the GitHub identity. Distinct from the per-repo `.substratum.toml` (config.py). Stdlib only."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
_DEFAULT_SERVER = os.environ.get("SUBSTRATUM_SERVER", "https://substratum-api.fly.dev")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _dir() -> str:
|
|
12
|
+
return os.path.join(os.path.expanduser("~"), ".substratum")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _path() -> str:
|
|
16
|
+
return os.path.join(_dir(), "config.json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load() -> dict:
|
|
20
|
+
try:
|
|
21
|
+
with open(_path(), encoding="utf-8") as fh:
|
|
22
|
+
d = json.load(fh)
|
|
23
|
+
return d if isinstance(d, dict) else {}
|
|
24
|
+
except Exception:
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save(**fields) -> None:
|
|
29
|
+
d = load()
|
|
30
|
+
d.update({k: v for k, v in fields.items() if v is not None})
|
|
31
|
+
os.makedirs(_dir(), exist_ok=True)
|
|
32
|
+
tmp = _path() + ".tmp"
|
|
33
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
34
|
+
json.dump(d, fh, indent=2)
|
|
35
|
+
os.replace(tmp, _path())
|
|
36
|
+
try:
|
|
37
|
+
os.chmod(_path(), 0o600) # the token is a credential
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear() -> None:
|
|
43
|
+
try:
|
|
44
|
+
os.remove(_path())
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def token() -> str:
|
|
50
|
+
return os.environ.get("SUBSTRATUM_TOKEN") or str(load().get("token") or "")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def server() -> str:
|
|
54
|
+
return str(load().get("server") or _DEFAULT_SERVER).rstrip("/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def github_login() -> str:
|
|
58
|
+
return str(load().get("github_login") or "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_logged_in() -> bool:
|
|
62
|
+
return bool(token())
|
substratum_cli/cli.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""The `substratum` CLI: argparse dispatch. Commands are pure (inputs -> SubstratumResult);
|
|
2
|
+
`main` alone maps the result to the process exit code and picks the renderer. Heavy commands
|
|
3
|
+
(fix/verify/init/...) are imported inside their branch so light commands (show/--help) never
|
|
4
|
+
pay the engine import.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from . import codes
|
|
13
|
+
from . import render as _render
|
|
14
|
+
from . import result as _result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _emit(res: "_result.SubstratumResult", args) -> int:
|
|
18
|
+
"""Render + return the process exit code. One place maps verdict -> exit."""
|
|
19
|
+
if getattr(args, "json", False):
|
|
20
|
+
print(_result.dumps(res))
|
|
21
|
+
else:
|
|
22
|
+
sys.stdout.write(_render.render_human(res, color=getattr(args, "color", None)))
|
|
23
|
+
return res.exit_code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _add_common(p: argparse.ArgumentParser) -> None:
|
|
27
|
+
p.add_argument("--json", action="store_true", help="emit the machine result (one object model)")
|
|
28
|
+
p.add_argument("--events", action="store_true", help="stream NDJSON stage events")
|
|
29
|
+
p.add_argument("--color", choices=["auto", "always", "never"], default="auto")
|
|
30
|
+
p.add_argument("-C", "--repo", default=".", help="repo checkout (default: cwd)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
ap = argparse.ArgumentParser(prog="substratum",
|
|
35
|
+
description="Substratum: verified-or-refused code, with provenance.")
|
|
36
|
+
# no subcommand -> the persistent session (cmd left None; handled in main)
|
|
37
|
+
sub = ap.add_subparsers(dest="cmd", required=False)
|
|
38
|
+
ps = sub.add_parser("session", help="interactive session: warm library, cd-in-and-drive")
|
|
39
|
+
_add_common(ps)
|
|
40
|
+
|
|
41
|
+
for name, help_ in [
|
|
42
|
+
("fix", "produce a verified fix for a failing test, or a named refusal"),
|
|
43
|
+
("write", "greenfield code from intent (derive-or-refuse; no test required)"),
|
|
44
|
+
("verify", "gate a diff (stdin/--diff/--staged/--commit); PASS/FAIL/per-hunk"),
|
|
45
|
+
("init", "mine the repo; print the Coverage Contract + flake scan + air-gap attestation"),
|
|
46
|
+
("apply", "apply a stored run's verified diff"),
|
|
47
|
+
("show", "render a stored run's transcript"),
|
|
48
|
+
("replay", "re-execute a stored run; assert byte-identical"),
|
|
49
|
+
("attest", "emit a signed evidence bundle for a run"),
|
|
50
|
+
("explain", "render the provenance manifest for a run"),
|
|
51
|
+
("doctor", "check harness, env, air-gap, effective config"),
|
|
52
|
+
("scaffold-test", "emit a runnable failing test pinning a target (the flip engine)"),
|
|
53
|
+
("summarize", "describe the codebase in plain English (grounded in real symbols)"),
|
|
54
|
+
("runs", "run history + the refusal->verified conversion ledger (the proof store)"),
|
|
55
|
+
("undo", "reverse-apply a run (the inverse of apply)"),
|
|
56
|
+
("commit", "proof-carrying git commit of a verified, applied run"),
|
|
57
|
+
("failing", "run the suite and list failing tests (each becomes a fix target)"),
|
|
58
|
+
]:
|
|
59
|
+
p = sub.add_parser(name, help=help_)
|
|
60
|
+
_add_common(p)
|
|
61
|
+
if name in ("fix", "scaffold-test", "failing"):
|
|
62
|
+
p.add_argument("target", nargs="?", help="failing test id / file::fn / path to scope")
|
|
63
|
+
if name == "failing":
|
|
64
|
+
p.add_argument("--test-command", default=None)
|
|
65
|
+
if name == "fix":
|
|
66
|
+
p.add_argument("--failing-test", action="append", default=[])
|
|
67
|
+
p.add_argument("--intent", default="")
|
|
68
|
+
p.add_argument("--test-command", default=None)
|
|
69
|
+
if name == "write":
|
|
70
|
+
p.add_argument("intent", help="what the code should do, in plain language")
|
|
71
|
+
p.add_argument("--sig", required=True, help="target signature, e.g. \"clamp(x, lo, hi)\"")
|
|
72
|
+
p.add_argument("--into", default=None, help="file to write into (default: <name>.py)")
|
|
73
|
+
p.add_argument("--mean", default=None,
|
|
74
|
+
help="disambiguate an ambiguous intent by the plain-English option text")
|
|
75
|
+
if name == "verify":
|
|
76
|
+
p.add_argument("target", nargs="?", default="-", help="'-' stdin, or a diff file")
|
|
77
|
+
p.add_argument("--diff", action="store_true")
|
|
78
|
+
p.add_argument("--staged", action="store_true")
|
|
79
|
+
p.add_argument("--commit", default=None)
|
|
80
|
+
p.add_argument("--test", action="append", default=[])
|
|
81
|
+
p.add_argument("--test-command", default=None)
|
|
82
|
+
if name in ("apply", "show", "replay", "attest", "explain", "undo", "commit"):
|
|
83
|
+
p.add_argument("run_id")
|
|
84
|
+
if name == "commit":
|
|
85
|
+
p.add_argument("--message", default=None)
|
|
86
|
+
if name == "explain":
|
|
87
|
+
p.add_argument("--line", default=None)
|
|
88
|
+
return ap
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def dispatch(args) -> "_result.SubstratumResult":
|
|
92
|
+
cmd = args.cmd
|
|
93
|
+
if cmd == "show":
|
|
94
|
+
from .commands.show import cmd_show
|
|
95
|
+
return cmd_show(args.repo, args.run_id)
|
|
96
|
+
# heavy commands land in later milestones; wire as built
|
|
97
|
+
if cmd == "verify":
|
|
98
|
+
from .commands.verify import cmd_verify
|
|
99
|
+
return cmd_verify(args)
|
|
100
|
+
if cmd == "fix":
|
|
101
|
+
from .commands.fix import cmd_fix
|
|
102
|
+
return cmd_fix(args)
|
|
103
|
+
if cmd == "write":
|
|
104
|
+
from .commands.write import cmd_write
|
|
105
|
+
return cmd_write(args)
|
|
106
|
+
if cmd == "apply":
|
|
107
|
+
from .commands.apply import cmd_apply
|
|
108
|
+
return cmd_apply(args.repo, args.run_id)
|
|
109
|
+
if cmd == "init":
|
|
110
|
+
from .commands.init_cmd import cmd_init
|
|
111
|
+
return cmd_init(args.repo)
|
|
112
|
+
if cmd == "doctor":
|
|
113
|
+
from .commands.init_cmd import cmd_doctor
|
|
114
|
+
return cmd_doctor(args.repo)
|
|
115
|
+
if cmd == "replay":
|
|
116
|
+
from .commands.replay import cmd_replay
|
|
117
|
+
return cmd_replay(args.repo, args.run_id)
|
|
118
|
+
if cmd == "explain":
|
|
119
|
+
from .commands.explain import cmd_explain
|
|
120
|
+
return cmd_explain(args.repo, args.run_id, getattr(args, "line", None))
|
|
121
|
+
if cmd == "attest":
|
|
122
|
+
from .commands.explain import cmd_attest
|
|
123
|
+
return cmd_attest(args.repo, args.run_id)
|
|
124
|
+
if cmd == "scaffold-test":
|
|
125
|
+
from .commands.scaffold import cmd_scaffold_test
|
|
126
|
+
return cmd_scaffold_test(args.repo, getattr(args, "target", None) or "")
|
|
127
|
+
if cmd == "summarize":
|
|
128
|
+
from . import product_ops
|
|
129
|
+
return product_ops.op_summarize_repo(args.repo)
|
|
130
|
+
if cmd == "runs":
|
|
131
|
+
from .commands.history import cmd_runs
|
|
132
|
+
return cmd_runs(args.repo)
|
|
133
|
+
if cmd == "undo":
|
|
134
|
+
from .commands.undo import cmd_undo
|
|
135
|
+
return cmd_undo(args.repo, args.run_id)
|
|
136
|
+
if cmd == "commit":
|
|
137
|
+
from .commands.commit import cmd_commit
|
|
138
|
+
return cmd_commit(args.repo, args.run_id, getattr(args, "message", None))
|
|
139
|
+
if cmd == "failing":
|
|
140
|
+
from .commands.failing import cmd_failing
|
|
141
|
+
return cmd_failing(args.repo, path=getattr(args, "target", None),
|
|
142
|
+
test_command=getattr(args, "test_command", None))
|
|
143
|
+
# not-yet-built commands: a clean, honest not-implemented refusal (never a crash)
|
|
144
|
+
return _result.make(codes.UNVERIFIABLE, "sub_000000000000", cmd,
|
|
145
|
+
code=codes.NO_DRIVER,
|
|
146
|
+
refusal=_not_built(cmd))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _not_built(cmd: str):
|
|
150
|
+
from . import refusals
|
|
151
|
+
return refusals.build(codes.NO_DRIVER, engine_code="NOT_IMPLEMENTED",
|
|
152
|
+
ctx={"target": cmd})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
156
|
+
argv = list(argv if argv is not None else sys.argv[1:])
|
|
157
|
+
# no arguments, or `session` -> the persistent interactive session
|
|
158
|
+
if not argv or argv[0] == "session":
|
|
159
|
+
from .session import run_session
|
|
160
|
+
repo = "."
|
|
161
|
+
rest = argv[1:] if argv and argv[0] == "session" else argv
|
|
162
|
+
if "-C" in rest:
|
|
163
|
+
i = rest.index("-C")
|
|
164
|
+
repo = rest[i + 1] if i + 1 < len(rest) else "."
|
|
165
|
+
elif "--repo" in rest:
|
|
166
|
+
i = rest.index("--repo")
|
|
167
|
+
repo = rest[i + 1] if i + 1 < len(rest) else "."
|
|
168
|
+
color = "--color" not in rest or "never" not in rest
|
|
169
|
+
return run_session(repo, color=color)
|
|
170
|
+
# account verbs are global (no repo, print directly) -- handled before the repo-based parser
|
|
171
|
+
if argv[0] in ("login", "logout", "whoami"):
|
|
172
|
+
from .commands import auth
|
|
173
|
+
return {"login": auth.cmd_login, "logout": auth.cmd_logout,
|
|
174
|
+
"whoami": auth.cmd_whoami}[argv[0]]()
|
|
175
|
+
ap = build_parser()
|
|
176
|
+
try:
|
|
177
|
+
args = ap.parse_args(argv)
|
|
178
|
+
except SystemExit as e:
|
|
179
|
+
return int(e.code) if e.code is not None else codes.EXIT_USAGE
|
|
180
|
+
try:
|
|
181
|
+
res = dispatch(args)
|
|
182
|
+
except Exception as exc: # internal fault -> exit 1, never a refusal code
|
|
183
|
+
sys.stderr.write(f"substratum: internal error: {exc}\n")
|
|
184
|
+
return codes.EXIT_INTERNAL
|
|
185
|
+
return _emit(res, args)
|
substratum_cli/codes.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Closed-class verdicts, refusal codes, the exit-code table, and the engine-code mapping.
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for the CLI contract. Nothing here imports the engine.
|
|
4
|
+
|
|
5
|
+
The exit-code ranges ARE the machine API (a script or the VS Code layer branches on the
|
|
6
|
+
process exit / the in-band `exit_code` field without parsing text):
|
|
7
|
+
|
|
8
|
+
0 VERIFIED | PASS the gate proved it
|
|
9
|
+
10-19 refusal classes a named, deliberate non-answer (never a crash)
|
|
10
|
+
20 FAIL a diff was proven WRONG by the gate
|
|
11
|
+
1 internal error the tool itself broke
|
|
12
|
+
2 usage error bad arguments
|
|
13
|
+
|
|
14
|
+
A refusal is a deliverable, so it NEVER shares the exit code of a crash (1). Cyan, not red.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
# ---- verdicts (closed) -------------------------------------------------------
|
|
19
|
+
VERIFIED = "VERIFIED"
|
|
20
|
+
PASS = "PASS"
|
|
21
|
+
FAIL = "FAIL"
|
|
22
|
+
REFUSED = "REFUSED"
|
|
23
|
+
UNVERIFIABLE = "UNVERIFIABLE"
|
|
24
|
+
VERDICTS = (VERIFIED, PASS, FAIL, REFUSED, UNVERIFIABLE)
|
|
25
|
+
|
|
26
|
+
# ---- refusal codes (closed class; ~a dozen) ----------------------------------
|
|
27
|
+
# Each maps to exactly one exit code in EXIT_CODES below. Grouped by exit class.
|
|
28
|
+
NO_GROUNDED_CANDIDATE = "NO_GROUNDED_CANDIDATE" # 10: coverage absent for this request
|
|
29
|
+
GATE_PRECONDITION = "GATE_PRECONDITION" # 11: the gate could not be run cleanly
|
|
30
|
+
NO_WARRANT_TESTS = "NO_WARRANT_TESTS" # 11: verify with no tests to gate against
|
|
31
|
+
HUNK_CONTEXT_STALE = "HUNK_CONTEXT_STALE" # 11: a diff hunk does not apply to base
|
|
32
|
+
APPLY_STATE_DRIFT = "APPLY_STATE_DRIFT" # 11: apply/replay: HEAD moved under the run
|
|
33
|
+
REPLAY_STATE_DRIFT = "REPLAY_STATE_DRIFT" # 11
|
|
34
|
+
NO_DRIVER = "NO_DRIVER" # 12: no gate runner / no filetype driver
|
|
35
|
+
AUTONOMY_LOCKED = "AUTONOMY_LOCKED" # 12: an L2/L3 action attempted at L1
|
|
36
|
+
UNDERDETERMINED_INTENT = "UNDERDETERMINED_INTENT" # 13: candidates composed, all failed the gate
|
|
37
|
+
FLAKY_OR_VACUOUS_WARRANT = "FLAKY_OR_VACUOUS_WARRANT" # 14: tests pass at base (nothing to fix / flaky)
|
|
38
|
+
FLAKY_TEST = "FLAKY_TEST" # 14: quarantined nondeterministic test
|
|
39
|
+
RUN_NOT_FOUND = "RUN_NOT_FOUND" # 11: show/explain/replay on an unknown id
|
|
40
|
+
NO_ENGINE = "NO_ENGINE" # 12: synthesis needs the engine; login or [engine]
|
|
41
|
+
|
|
42
|
+
REFUSAL_CODES = (
|
|
43
|
+
NO_GROUNDED_CANDIDATE, GATE_PRECONDITION, NO_WARRANT_TESTS, HUNK_CONTEXT_STALE,
|
|
44
|
+
APPLY_STATE_DRIFT, REPLAY_STATE_DRIFT, NO_DRIVER, AUTONOMY_LOCKED,
|
|
45
|
+
UNDERDETERMINED_INTENT, FLAKY_OR_VACUOUS_WARRANT, FLAKY_TEST, RUN_NOT_FOUND,
|
|
46
|
+
NO_ENGINE,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# ---- the exit-code table (closed; the machine contract) ----------------------
|
|
50
|
+
EXIT_OK = 0
|
|
51
|
+
EXIT_FAIL = 20
|
|
52
|
+
EXIT_INTERNAL = 1
|
|
53
|
+
EXIT_USAGE = 2
|
|
54
|
+
|
|
55
|
+
_REFUSAL_EXIT = {
|
|
56
|
+
NO_GROUNDED_CANDIDATE: 10,
|
|
57
|
+
GATE_PRECONDITION: 11,
|
|
58
|
+
NO_WARRANT_TESTS: 11,
|
|
59
|
+
HUNK_CONTEXT_STALE: 11,
|
|
60
|
+
APPLY_STATE_DRIFT: 11,
|
|
61
|
+
REPLAY_STATE_DRIFT: 11,
|
|
62
|
+
RUN_NOT_FOUND: 11,
|
|
63
|
+
NO_DRIVER: 12,
|
|
64
|
+
AUTONOMY_LOCKED: 12,
|
|
65
|
+
NO_ENGINE: 12,
|
|
66
|
+
UNDERDETERMINED_INTENT: 13,
|
|
67
|
+
FLAKY_OR_VACUOUS_WARRANT: 14,
|
|
68
|
+
FLAKY_TEST: 14,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def exit_code_for(verdict: str, code: str | None) -> int:
|
|
73
|
+
"""The one place verdict+code -> process exit. Total over the closed classes."""
|
|
74
|
+
if verdict in (VERIFIED, PASS):
|
|
75
|
+
return EXIT_OK
|
|
76
|
+
if verdict == FAIL:
|
|
77
|
+
return EXIT_FAIL
|
|
78
|
+
if verdict in (REFUSED, UNVERIFIABLE):
|
|
79
|
+
return _REFUSAL_EXIT.get(code or "", 10) # unknown refusal -> the generic 10
|
|
80
|
+
return EXIT_INTERNAL
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---- engine-code -> CLI refusal code (the single mapping door) ----------------
|
|
84
|
+
# Engine/gate codes (client_gate.ClientGateResult.code, resolve_files refusal_sink codes,
|
|
85
|
+
# substrate_query trace statuses) are translated here and NOWHERE else. Unknown engine
|
|
86
|
+
# codes map to GATE_PRECONDITION with the raw code preserved by the caller (refuse-on-unknown,
|
|
87
|
+
# never invent a template).
|
|
88
|
+
_ENGINE_TO_CLI = {
|
|
89
|
+
# client_gate closed set
|
|
90
|
+
"PASS": None, # not a refusal
|
|
91
|
+
"TEST_FAILED": None, # -> FAIL verdict, handled by caller
|
|
92
|
+
"GATE_TESTS_PASS_AT_BASE": FLAKY_OR_VACUOUS_WARRANT,
|
|
93
|
+
"GATE_NO_RUNNER": NO_DRIVER,
|
|
94
|
+
"GATE_WORKTREE_FAILED": GATE_PRECONDITION,
|
|
95
|
+
"GATE_BASE_ERROR": GATE_PRECONDITION,
|
|
96
|
+
"GATE_ERROR": GATE_PRECONDITION,
|
|
97
|
+
"GATE_TIMEOUT": GATE_PRECONDITION,
|
|
98
|
+
# substrate_query / resolve_files trace + refusal_sink
|
|
99
|
+
"no_candidate": NO_GROUNDED_CANDIDATE,
|
|
100
|
+
"candidate_failed_verify": UNDERDETERMINED_INTENT,
|
|
101
|
+
"candidate_failed_execution_gate": UNDERDETERMINED_INTENT,
|
|
102
|
+
"no_op_candidate": NO_GROUNDED_CANDIDATE,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def map_engine_code(engine_code: str) -> str:
|
|
107
|
+
"""Translate a raw engine/gate code to a CLI refusal code. NONCODE_NO_DRIVER(...) and
|
|
108
|
+
NONCODE_TOOLCHAIN_OWNED(...) are prefix-matched. Unknown -> GATE_PRECONDITION."""
|
|
109
|
+
if engine_code in _ENGINE_TO_CLI and _ENGINE_TO_CLI[engine_code] is not None:
|
|
110
|
+
return _ENGINE_TO_CLI[engine_code]
|
|
111
|
+
if engine_code.startswith(("NONCODE_NO_DRIVER", "NONCODE_TOOLCHAIN_OWNED",
|
|
112
|
+
"NONCODE_UNKNOWN_FILETYPE")):
|
|
113
|
+
return NO_DRIVER
|
|
114
|
+
return GATE_PRECONDITION
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""`substratum apply <run-id>` -- apply a stored run's verified diff. L1 default: apply is
|
|
2
|
+
the ONLY writer. Refuses APPLY_STATE_DRIFT if HEAD has moved since the run was produced."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
from .. import codes
|
|
10
|
+
from .. import gitio
|
|
11
|
+
from .. import refusals
|
|
12
|
+
from .. import result as _result
|
|
13
|
+
from .. import run_store
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_apply(repo: str, run_id: str) -> "_result.SubstratumResult":
|
|
17
|
+
repo = os.path.abspath(repo)
|
|
18
|
+
loaded = run_store.load_run(repo, run_id)
|
|
19
|
+
if loaded is None:
|
|
20
|
+
return _result.make(codes.REFUSED, run_id, "apply", code=codes.RUN_NOT_FOUND,
|
|
21
|
+
refusal=refusals.build(codes.RUN_NOT_FOUND,
|
|
22
|
+
ctx={"target": run_id, "run_id": run_id}))
|
|
23
|
+
res, inputs = loaded
|
|
24
|
+
recorded_head = (inputs.get("run_key") or {}).get("head_sha")
|
|
25
|
+
if recorded_head and gitio.head_sha(repo) != recorded_head:
|
|
26
|
+
return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
|
|
27
|
+
refusal=refusals.build(codes.APPLY_STATE_DRIFT,
|
|
28
|
+
ctx={"target": run_id, "run_id": run_id}))
|
|
29
|
+
if not res.diff.strip():
|
|
30
|
+
return _result.make(codes.REFUSED, run_id, "apply", code=codes.NO_GROUNDED_CANDIDATE,
|
|
31
|
+
refusal=refusals.build(codes.NO_GROUNDED_CANDIDATE,
|
|
32
|
+
ctx={"target": run_id, "run_id": run_id}))
|
|
33
|
+
with tempfile.NamedTemporaryFile("w", suffix=".patch", delete=False) as fh:
|
|
34
|
+
fh.write(res.diff)
|
|
35
|
+
patch = fh.name
|
|
36
|
+
try:
|
|
37
|
+
r = subprocess.run(["git", "-C", repo, "apply", "--index", patch],
|
|
38
|
+
capture_output=True, text=True)
|
|
39
|
+
if r.returncode != 0:
|
|
40
|
+
r2 = subprocess.run(["git", "-C", repo, "apply", "--3way", patch],
|
|
41
|
+
capture_output=True, text=True)
|
|
42
|
+
if r2.returncode != 0:
|
|
43
|
+
return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
|
|
44
|
+
refusal=refusals.build(codes.APPLY_STATE_DRIFT,
|
|
45
|
+
ctx={"target": run_id, "run_id": run_id}))
|
|
46
|
+
finally:
|
|
47
|
+
os.unlink(patch)
|
|
48
|
+
return _result.make(codes.PASS, run_id, "apply", files=res.files,
|
|
49
|
+
data={"applied": list(res.files)})
|