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,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"
@@ -0,0 +1,7 @@
1
+ """python -m substratum_cli -> the CLI."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main(sys.argv[1:]))
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)
@@ -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)})