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,193 @@
1
+ """`substratum verify` -- gate ANY diff (Cursor/Copilot output, a paste, a commit). The wedge.
2
+
3
+ Reads a unified diff from stdin/-/--diff/--staged/--commit, materializes it against the base,
4
+ runs the differential client gate (fail-at-base -> pass-with-patch), and for a multi-file diff
5
+ attributes a per-hunk verdict. The gate proves BEHAVIOR, never authorship -- the `note:` line
6
+ keeps that honest. This command leaf-loads client_gate; it never pays the numpy chain.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from typing import Optional
13
+
14
+ from .. import codes
15
+ from .. import diffs
16
+ from .. import engine
17
+ from .. import gitio
18
+ from .. import refusals
19
+ from .. import result as _result
20
+ from .. import run_store
21
+ from .. import runid
22
+
23
+
24
+ def _read_diff(args) -> str:
25
+ if args.staged:
26
+ return gitio.staged_diff(os.path.abspath(args.repo))
27
+ if args.commit:
28
+ return gitio.commit_diff(os.path.abspath(args.repo), args.commit)
29
+ src = getattr(args, "target", "-") or "-"
30
+ if args.diff or (src != "-" and os.path.isfile(src)):
31
+ path = src if src != "-" else None
32
+ if path:
33
+ return open(path, encoding="utf-8", errors="replace").read()
34
+ return sys.stdin.read()
35
+
36
+
37
+ def _base_content(repo: str, base: str, path: str) -> str:
38
+ return gitio.show(repo, base, path) or ""
39
+
40
+
41
+ def cmd_verify(args) -> "_result.SubstratumResult":
42
+ repo = os.path.abspath(args.repo)
43
+ if not gitio.is_git_repo(repo):
44
+ rid, _ = runid.compute(repo, "verify", {"err": "not_git"})
45
+ return _result.make(codes.REFUSED, rid, "verify", code=codes.GATE_PRECONDITION,
46
+ refusal=refusals.build(codes.GATE_PRECONDITION,
47
+ ctx={"target": repo, "run_id": rid}))
48
+ base = args.commit + "~1" if args.commit else gitio.head_sha(repo)
49
+ diff_text = _read_diff(args)
50
+ return run_verify(repo, diff_text, tuple(args.test or ()), base=base,
51
+ test_command=args.test_command)
52
+
53
+
54
+ def run_verify(repo, diff_text, tests, base=None, test_command=None):
55
+ """The verify core, callable directly (the session gates the working diff with this)."""
56
+ repo = os.path.abspath(repo)
57
+ tests = tuple(tests or ())
58
+ if base is None:
59
+ base = gitio.head_sha(repo)
60
+ rid, key = runid.compute(repo, "verify",
61
+ {"diff_sha": _sha(diff_text), "tests": sorted(tests),
62
+ "base": base, "test_cmd": test_command})
63
+
64
+ files = diffs.parse(diff_text)
65
+ if not files:
66
+ return _store(repo, _result.make(codes.REFUSED, rid, "verify",
67
+ code=codes.NO_WARRANT_TESTS,
68
+ refusal=refusals.build(codes.NO_WARRANT_TESTS,
69
+ ctx={"target": "the diff", "run_id": rid})), key)
70
+ if not tests:
71
+ return _store(repo, _result.make(codes.UNVERIFIABLE, rid, "verify",
72
+ code=codes.NO_WARRANT_TESTS,
73
+ refusal=refusals.build(codes.NO_WARRANT_TESTS,
74
+ ctx={"target": files[0].path, "run_id": rid})), key)
75
+
76
+ base_by_path = {f.path: _base_content(repo, base, f.path) for f in files}
77
+ edits, stale = diffs.materialize(base_by_path, files)
78
+ if not edits:
79
+ return _store(repo, _result.make(codes.UNVERIFIABLE, rid, "verify",
80
+ code=codes.HUNK_CONTEXT_STALE,
81
+ refusal=refusals.build(codes.HUNK_CONTEXT_STALE,
82
+ ctx={"target": ", ".join(stale), "run_id": rid})), key)
83
+
84
+ # ---- headline gate: the whole diff, differential ----
85
+ sink: dict = {}
86
+ cg = engine.client_gate.run_client_gate(repo, base, edits, test_paths=list(tests),
87
+ test_cmd=test_command, differential=True,
88
+ transcript_sink=sink)
89
+ gate = _gate_transcript(sink, tests, cg)
90
+
91
+ if cg.code == "GATE_TESTS_PASS_AT_BASE":
92
+ return _store(repo, _result.make(codes.REFUSED, rid, "verify",
93
+ code=codes.FLAKY_OR_VACUOUS_WARRANT, gate=gate,
94
+ refusal=refusals.build(codes.FLAKY_OR_VACUOUS_WARRANT,
95
+ ctx={"target": ", ".join(tests), "run_id": rid},
96
+ verified_before=(f"base run: {gate.base_status}",))), key)
97
+ if not cg.passed and cg.code != "TEST_FAILED":
98
+ ref = refusals.from_engine(cg.code, ctx={"target": ", ".join(tests), "run_id": rid})
99
+ return _store(repo, _result.make(codes.UNVERIFIABLE, rid, "verify",
100
+ code=ref.code, gate=gate, refusal=ref), key)
101
+
102
+ # ---- per-hunk attribution (multi-file / multi-hunk) ----
103
+ hunks_verdicts = _attribute(repo, base, base_by_path, files, edits, tests,
104
+ test_command, whole_passed=cg.passed)
105
+ note = "this verdict gates the DIFF, not its provenance (external code)."
106
+ if cg.passed:
107
+ verdict, code = codes.PASS, None
108
+ else:
109
+ verdict, code = codes.FAIL, None # TEST_FAILED -> diff proven wrong
110
+ res = _result.make(verdict, rid, "verify", code=code, gate=gate,
111
+ files=tuple(sorted(edits)), hunks=tuple(hunks_verdicts),
112
+ data={"note": note, "stale": stale})
113
+ return _store(repo, res, key)
114
+
115
+
116
+ def _attribute(repo, base, base_by_path, files, edits, tests, test_cmd, whole_passed):
117
+ """Per-file leave-one-out (whole passed) or singleton (whole failed) attribution, budgeted."""
118
+ out = []
119
+ multi = sum(len(f.hunks) for f in files) > 1
120
+ if not multi:
121
+ f = files[0]
122
+ out.append(_result.HunkVerdict(f.path, 0, f.hunks[0].header,
123
+ codes.PASS if whole_passed else codes.FAIL))
124
+ return out
125
+ budget = 8
126
+ runs = 0
127
+ for f in files:
128
+ if f.path not in edits:
129
+ out.append(_result.HunkVerdict(f.path, 0, "stale", codes.UNVERIFIABLE,
130
+ code=codes.HUNK_CONTEXT_STALE,
131
+ next_command=f"substratum scaffold-test {f.path}"))
132
+ continue
133
+ if runs >= budget:
134
+ out.append(_result.HunkVerdict(f.path, 0, f.hunks[0].header,
135
+ codes.PASS if whole_passed else codes.UNVERIFIABLE,
136
+ code=None if whole_passed else codes.UNDERDETERMINED_INTENT))
137
+ continue
138
+ if whole_passed:
139
+ minus = {p: c for p, c in edits.items() if p != f.path}
140
+ runs += 1
141
+ cg = engine.client_gate.run_client_gate(repo, base, minus, test_paths=list(tests),
142
+ test_cmd=test_cmd, differential=True)
143
+ # ablation FAILS tests -> this file is load-bearing -> PASS
144
+ load_bearing = (not cg.passed and cg.code == "TEST_FAILED")
145
+ out.append(_result.HunkVerdict(
146
+ f.path, 0, f.hunks[0].header,
147
+ codes.PASS if load_bearing else codes.UNVERIFIABLE,
148
+ code=None if load_bearing else codes.UNDERDETERMINED_INTENT,
149
+ next_command=None if load_bearing else f"substratum scaffold-test {f.path}"))
150
+ else:
151
+ only = {f.path: edits[f.path]}
152
+ runs += 1
153
+ cg = engine.client_gate.run_client_gate(repo, base, only, test_paths=list(tests),
154
+ test_cmd=test_cmd, differential=True)
155
+ if cg.passed:
156
+ out.append(_result.HunkVerdict(f.path, 0, f.hunks[0].header, codes.PASS))
157
+ elif cg.code == "TEST_FAILED":
158
+ out.append(_result.HunkVerdict(f.path, 0, f.hunks[0].header, codes.FAIL))
159
+ else:
160
+ out.append(_result.HunkVerdict(f.path, 0, f.hunks[0].header, codes.UNVERIFIABLE,
161
+ code=codes.map_engine_code(cg.code)))
162
+ return out
163
+
164
+
165
+ def _gate_transcript(sink: dict, tests, cg) -> "_result.GateTranscript":
166
+ base_rc = sink.get("base_rc")
167
+ patched_rc = sink.get("patched_rc")
168
+ return _result.GateTranscript(
169
+ kind="client_differential", test_ids=tuple(tests),
170
+ runner_command=tuple(sink.get("runner_command", ())),
171
+ base_status=_status(base_rc, at_base=True),
172
+ patched_status=_status(patched_rc, at_base=False),
173
+ base_excerpt=(sink.get("base_out", "") or "")[-800:],
174
+ patched_excerpt=(sink.get("patched_out", "") or "")[-800:],
175
+ )
176
+
177
+
178
+ def _status(rc: Optional[int], at_base: bool) -> str:
179
+ if rc is None:
180
+ return ""
181
+ if at_base:
182
+ return "FAIL" if rc != 0 else "PASS"
183
+ return "PASS" if rc == 0 else "FAIL"
184
+
185
+
186
+ def _store(repo, res, key):
187
+ run_store.store_run(repo, res, run_key=key)
188
+ return res
189
+
190
+
191
+ def _sha(text: str) -> str:
192
+ import hashlib
193
+ return hashlib.sha256(text.encode("utf-8", "replace")).hexdigest()[:16]
@@ -0,0 +1,42 @@
1
+ """`substratum write "<intent>" --sig "name(args)"` -- greenfield code from intent.
2
+
3
+ Derivation, not generation: the intent is comprehended, the construction is derived from the
4
+ granular productions + grounding, verified intrinsically, and emitted or refused. No test is
5
+ required and none determines the answer -- resolve iff the intent is fully understood and the
6
+ construction is derivable, else a named refusal. Thin over product_ops.op_write_from_intent."""
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+
12
+ from .. import codes
13
+ from .. import gitio
14
+ from .. import refusals
15
+ from .. import result as _result
16
+
17
+
18
+ def cmd_write(args) -> "_result.SubstratumResult":
19
+ from .. import product_ops
20
+ repo = os.path.abspath(args.repo)
21
+ if not gitio.is_git_repo(repo):
22
+ return _result.make(codes.REFUSED, "sub_000000000000", "write",
23
+ code=codes.GATE_PRECONDITION,
24
+ refusal=refusals.build(codes.GATE_PRECONDITION,
25
+ ctx={"target": repo, "run_id": "sub_000000000000"}))
26
+ intent = getattr(args, "intent", "") or ""
27
+ if intent and os.path.isfile(intent):
28
+ intent = open(intent, encoding="utf-8", errors="replace").read()
29
+
30
+ emit = None
31
+ if getattr(args, "events", False):
32
+ import json
33
+
34
+ def emit(stage, text):
35
+ sys.stdout.write(json.dumps({"stage": stage, "text": text}) + "\n"); sys.stdout.flush()
36
+ elif not getattr(args, "json", False):
37
+ def emit(stage, text):
38
+ sys.stderr.write(f" {stage:7} {text}\n"); sys.stderr.flush()
39
+
40
+ return product_ops.op_write_from_intent(
41
+ repo, intent, getattr(args, "sig", ""), into=getattr(args, "into", None), emit=emit,
42
+ prefer_purpose=getattr(args, "mean", None))
@@ -0,0 +1,49 @@
1
+ """`.substratum.toml` reader. Autonomy ladder: L1 Suggest is the default (print, never write;
2
+ apply is the only writer). L2/L3 are PARSED but LOCKED in v1 -- an L2/L3 action refuses
3
+ AUTONOMY_LOCKED. Config keys: autonomy, output, drivers, limits. Stdlib tomllib only."""
4
+ from __future__ import annotations
5
+
6
+ import os
7
+
8
+ try:
9
+ import tomllib # py3.11+
10
+ except ModuleNotFoundError: # pragma: no cover
11
+ tomllib = None
12
+
13
+ _DEFAULTS = {
14
+ "autonomy": {"level": "L1", "automerge_classes": []},
15
+ "output": {"refusal_verbosity": "always"},
16
+ "drivers": {},
17
+ "limits": {"attempts_per_target": 3, "runner_timeout": 600, "max_gate_runs": 8},
18
+ }
19
+
20
+
21
+ def load(repo: str) -> dict:
22
+ cfg = {k: dict(v) if isinstance(v, dict) else v for k, v in _DEFAULTS.items()}
23
+ path = os.path.join(repo, ".substratum.toml")
24
+ if tomllib and os.path.exists(path):
25
+ try:
26
+ with open(path, "rb") as fh:
27
+ user = tomllib.load(fh)
28
+ for section, vals in user.items():
29
+ if section in cfg and isinstance(cfg[section], dict) and isinstance(vals, dict):
30
+ cfg[section].update(vals)
31
+ else:
32
+ cfg[section] = vals
33
+ except Exception:
34
+ pass
35
+ return cfg
36
+
37
+
38
+ def autonomy_level(repo: str) -> str:
39
+ return str(load(repo).get("autonomy", {}).get("level", "L1")).upper()
40
+
41
+
42
+ def is_write_allowed(repo: str, action: str) -> bool:
43
+ """L1: only `apply` (an explicit user action) writes. L2/L3 are locked in v1."""
44
+ level = autonomy_level(repo)
45
+ if action == "apply":
46
+ return True # apply is always an explicit, user-invoked write
47
+ if level in ("L2", "L3"):
48
+ return False # visible-but-locked: the config declares ambition, not power
49
+ return False
@@ -0,0 +1,106 @@
1
+ """Unified-diff parsing into per-file hunks, and materialization of patched whole-file content
2
+ (the gate takes {path: content}, not hunks). Stdlib only."""
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from typing import Dict, List, Optional, Tuple
8
+
9
+ _FILE_RE = re.compile(r"^\+\+\+ b/(.+)$")
10
+ _HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
11
+
12
+
13
+ @dataclass
14
+ class Hunk:
15
+ header: str
16
+ old_start: int
17
+ old_len: int
18
+ new_start: int
19
+ new_len: int
20
+ lines: List[str] = field(default_factory=list) # with leading ' '/'+'/'-'
21
+
22
+
23
+ @dataclass
24
+ class FileDiff:
25
+ path: str
26
+ hunks: List[Hunk] = field(default_factory=list)
27
+
28
+
29
+ def parse(diff_text: str) -> List[FileDiff]:
30
+ files: List[FileDiff] = []
31
+ cur: Optional[FileDiff] = None
32
+ cur_hunk: Optional[Hunk] = None
33
+ for line in diff_text.splitlines():
34
+ m = _FILE_RE.match(line)
35
+ if m:
36
+ cur = FileDiff(path=m.group(1))
37
+ files.append(cur)
38
+ cur_hunk = None
39
+ continue
40
+ if line.startswith("--- ") or line.startswith("diff --git") or line.startswith("index "):
41
+ continue
42
+ hm = _HUNK_RE.match(line)
43
+ if hm and cur is not None:
44
+ cur_hunk = Hunk(header=line,
45
+ old_start=int(hm.group(1)), old_len=int(hm.group(2) or 1),
46
+ new_start=int(hm.group(3)), new_len=int(hm.group(4) or 1))
47
+ cur.hunks.append(cur_hunk)
48
+ continue
49
+ if cur_hunk is not None and (not line or line[0] in " +-\\"):
50
+ cur_hunk.lines.append(line)
51
+ return [f for f in files if f.hunks]
52
+
53
+
54
+ def apply_hunks(base_content: str, hunks: List[Hunk]) -> Optional[str]:
55
+ """Apply hunks to base with EXACT context matching. Returns new content, or None if any
56
+ hunk's context does not match (context stale -> the caller refuses, never fuzzes)."""
57
+ base_lines = base_content.splitlines(keepends=False)
58
+ out: List[str] = []
59
+ cursor = 0 # index into base_lines already consumed
60
+ for h in sorted(hunks, key=lambda x: x.old_start):
61
+ start = h.old_start - 1 # 0-indexed
62
+ if start < cursor or start > len(base_lines):
63
+ return None
64
+ out.extend(base_lines[cursor:start])
65
+ bi = start
66
+ for dl in h.lines:
67
+ if not dl:
68
+ # blank context line
69
+ if bi < len(base_lines) and base_lines[bi] == "":
70
+ out.append("")
71
+ bi += 1
72
+ continue
73
+ tag, text = dl[0], dl[1:]
74
+ if tag == " ":
75
+ if bi >= len(base_lines) or base_lines[bi] != text:
76
+ return None
77
+ out.append(text)
78
+ bi += 1
79
+ elif tag == "-":
80
+ if bi >= len(base_lines) or base_lines[bi] != text:
81
+ return None
82
+ bi += 1
83
+ elif tag == "+":
84
+ out.append(text)
85
+ elif tag == "\\":
86
+ pass # ""
87
+ cursor = bi
88
+ out.extend(base_lines[cursor:])
89
+ trailing = "\n" if base_content.endswith("\n") else ""
90
+ return "\n".join(out) + trailing
91
+
92
+
93
+ def materialize(base_by_path: Dict[str, str],
94
+ files: List[FileDiff]) -> Tuple[Dict[str, str], List[str]]:
95
+ """{path: patched content} for every file whose hunks apply cleanly; the second list is
96
+ paths with stale context (excluded, never fuzzed)."""
97
+ edits: Dict[str, str] = {}
98
+ stale: List[str] = []
99
+ for f in files:
100
+ base = base_by_path.get(f.path, "")
101
+ new = apply_hunks(base, f.hunks)
102
+ if new is None:
103
+ stale.append(f.path)
104
+ else:
105
+ edits[f.path] = new
106
+ return edits, stale
@@ -0,0 +1,154 @@
1
+ """The lazy engine bridge -- the ONLY module in substratum_cli that reaches into backend.*.
2
+
3
+ The import bomb: `backend/substratum_v2/__init__.py` eagerly imports the numpy-heavy claim
4
+ pipeline, so a normal `from backend...` costs that chain (and dies without numpy). Two
5
+ strategies keep light commands fast and numpy-free:
6
+
7
+ 1. LEAF LOAD by file path (importlib.util.spec_from_file_location) for modules verified to be
8
+ stdlib-only with NO relative imports: `client_gate.py`, `lib_roots.py`. These are loaded
9
+ without ever executing any `backend` package __init__, so `verify` and run-id hashing pay
10
+ nothing.
11
+ 2. HEAVY IMPORT via the normal package path, done INSIDE the fix code path only, after the
12
+ first progress event has streamed (the user sees GROUND before the seconds-scale import).
13
+
14
+ Every attribute here is a lazy accessor; importing this module opens nothing.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+ import os
20
+ import sys
21
+ from types import ModuleType
22
+
23
+ REPO_ROOT = os.environ.get("SUBSTRATUM_ENGINE_ROOT", "/home/thepadrevictor/Substratum")
24
+ _ENGINE_DIR = os.path.join(REPO_ROOT, "backend", "substratum_v2", "code_generation_v2")
25
+ _LEAF_CACHE: dict[str, ModuleType] = {}
26
+
27
+
28
+ def _leaf(name: str) -> ModuleType:
29
+ """Load `<engine>/<name>.py` in isolation (no backend __init__), cached."""
30
+ if name in _LEAF_CACHE:
31
+ return _LEAF_CACHE[name]
32
+ path = os.path.join(_ENGINE_DIR, f"{name}.py")
33
+ spec = importlib.util.spec_from_file_location(f"_substratum_leaf_{name}", path)
34
+ if spec is None or spec.loader is None:
35
+ raise ImportError(f"cannot leaf-load {path}")
36
+ mod = importlib.util.module_from_spec(spec)
37
+ # register so dataclasses/pickling inside the module resolve their own module name
38
+ sys.modules[spec.name] = mod
39
+ spec.loader.exec_module(mod)
40
+ _LEAF_CACHE[name] = mod
41
+ return mod
42
+
43
+
44
+ class _LazyAttr:
45
+ """Access `.foo` -> leaf-load module `name` and return its `foo` (or the module if attr=None)."""
46
+ def __init__(self, name: str):
47
+ self._name = name
48
+
49
+ def __getattr__(self, attr: str):
50
+ return getattr(_leaf(self._name), attr)
51
+
52
+
53
+ # ---- leaf modules (fast, numpy-free) ----
54
+ client_gate = _LazyAttr("client_gate") # run_client_gate, resolve_test_command, ClientGateResult
55
+ lib_roots = _LazyAttr("lib_roots") # py_/go_/ts_library_root, defaults
56
+
57
+
58
+ def coverage_registries() -> dict:
59
+ """The LIVE driver registries for the Coverage Contract, read from the engine (not copied,
60
+ so no drift). All leaf-loaded -> zero numpy. Returns the extension buckets."""
61
+ nc = _leaf("noncode_drivers")
62
+ out = {"driven_noncode": set(getattr(nc, "_DRIVERS", {}).keys()),
63
+ "template_exts": set(), "known_undriven": set(getattr(nc, "_KNOWN_UNDRIVEN", ())),
64
+ "code_langs": {".py": "py", ".go": "go", ".ts": "ts", ".tsx": "ts",
65
+ ".js": "ts", ".jsx": "ts"}}
66
+ try:
67
+ out["driven_noncode"] |= set(getattr(_leaf("struct_resolve"), "STRUCT_EXTS", ()))
68
+ except Exception:
69
+ pass
70
+ try:
71
+ out["template_exts"] = set(getattr(_leaf("template_resolve"), "TEMPLATE_EXTS", ()))
72
+ except Exception:
73
+ pass
74
+ return out
75
+
76
+
77
+ def canonical_diff():
78
+ """canonical_diff_builder.build_file_diff -- audited for import-cleanliness at call time;
79
+ falls back to the heavy package path if it has relative imports."""
80
+ try:
81
+ return _leaf("canonical_diff_builder").build_file_diff
82
+ except Exception:
83
+ _ensure_repo_on_path()
84
+ from backend.substratum_v2.code_generation_v2.canonical_diff_builder import build_file_diff
85
+ return build_file_diff
86
+
87
+
88
+ # ---- heavy path (numpy chain paid here, inside the fix op only) ----
89
+ def _ensure_repo_on_path():
90
+ if REPO_ROOT not in sys.path:
91
+ sys.path.insert(0, REPO_ROOT)
92
+
93
+
94
+ def available() -> bool:
95
+ """Is the heavy engine importable here? A client-only (`pip install substratum-cli`) install
96
+ has no numpy, so numpy is the cheap discriminator between the thin client and a full engine
97
+ checkout. When False, the fix/write local path refuses cleanly instead of crashing."""
98
+ try:
99
+ import numpy # noqa: F401
100
+ return os.path.isdir(_ENGINE_DIR)
101
+ except Exception:
102
+ return False
103
+
104
+
105
+ _HEAVY: dict | None = None
106
+ _LIB_CACHE: dict[str, object] = {}
107
+
108
+
109
+ def heavy():
110
+ """Return a namespace of the heavy fix-path symbols. Pays the numpy chain ONCE per
111
+ process (cached), so a persistent session reuses the imports across every command."""
112
+ global _HEAVY
113
+ if _HEAVY is None:
114
+ _ensure_repo_on_path()
115
+ from backend.substratum_v2.code_generation_v2 import substrates # noqa: F401 (populates the registry)
116
+ from backend.substratum_v2.code_generation_v2.substrate_query import (
117
+ FixRequest, run_substrate_query)
118
+ from backend.substratum_v2.code_generation_v2.pattern_library_lazy import load_library_lazy
119
+ from backend.substratum_v2.code_generation_v2.v0_synth_entry import (
120
+ synthesize, synthesize_from_intent)
121
+ _HEAVY = {
122
+ "FixRequest": FixRequest,
123
+ "run_substrate_query": run_substrate_query,
124
+ "load_library_lazy": load_library_lazy,
125
+ "synthesize": synthesize,
126
+ "synthesize_from_intent": synthesize_from_intent,
127
+ }
128
+ return _HEAVY
129
+
130
+
131
+ def load_library_cached(lib_root: str):
132
+ """Load the library resident index ONCE per (process, root). The lazy loader hydrates
133
+ patterns on demand and keeps its own LRU, so a warm session pays the resident load once
134
+ and reuses hydration across every fix. This is the warm-library win."""
135
+ if lib_root not in _LIB_CACHE:
136
+ _LIB_CACHE[lib_root] = heavy()["load_library_lazy"](lib_root)
137
+ return _LIB_CACHE[lib_root]
138
+
139
+
140
+ def is_warm(lib_root: str | None = None) -> bool:
141
+ if lib_root is None:
142
+ lib_root = lib_roots.py_library_root()
143
+ return bool(_HEAVY is not None and lib_root and lib_root in _LIB_CACHE)
144
+
145
+
146
+ def warm(lib_root: str | None = None) -> bool:
147
+ """Preload the heavy imports + the library so the first fix in a session is fast.
148
+ Safe to call from a background thread at session start."""
149
+ if lib_root is None:
150
+ lib_root = lib_roots.py_library_root()
151
+ if not lib_root:
152
+ return False
153
+ load_library_cached(lib_root)
154
+ return True
@@ -0,0 +1,102 @@
1
+ """Read-only git helpers. Stdlib + the `git` binary only; never writes to the object DB
2
+ except `git hash-object` WITHOUT -w (which computes a hash without storing)."""
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from typing import List, Optional
7
+
8
+
9
+ def _git(repo: str, *args: str, timeout: int = 60) -> subprocess.CompletedProcess:
10
+ return subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True,
11
+ timeout=timeout)
12
+
13
+
14
+ def is_git_repo(repo: str) -> bool:
15
+ import os
16
+ return os.path.isdir(os.path.join(repo, ".git")) or \
17
+ _git(repo, "rev-parse", "--git-dir").returncode == 0
18
+
19
+
20
+ def apply_reverse(repo: str, diff_text: str) -> tuple[bool, str]:
21
+ """Reverse-apply a stored diff (the exact inverse of `substratum apply`). Returns (ok, message).
22
+ Uses --3way so it undoes even if surrounding lines moved; refuses cleanly if it can't."""
23
+ if not diff_text.strip():
24
+ return False, "empty diff"
25
+ r = subprocess.run(["git", "-C", repo, "apply", "--reverse", "--3way", "-"],
26
+ input=diff_text, capture_output=True, text=True, timeout=60)
27
+ if r.returncode == 0:
28
+ return True, "reverted"
29
+ r2 = subprocess.run(["git", "-C", repo, "apply", "--reverse", "-"],
30
+ input=diff_text, capture_output=True, text=True, timeout=60)
31
+ if r2.returncode == 0:
32
+ return True, "reverted"
33
+ err = (r.stderr or r2.stderr or "").strip().splitlines()
34
+ return False, (err[-1] if err else "could not reverse-apply (the files changed since apply)")
35
+
36
+
37
+ def commit_files(repo: str, files, message: str) -> tuple[bool, str]:
38
+ """Stage the given files and commit them with `message`. Returns (ok, short_sha_or_error).
39
+ Uses the repo's own git author config (never Substratum's)."""
40
+ files = list(files or [])
41
+ if not files:
42
+ return False, "no files to commit"
43
+ add = _git(repo, "add", "--", *files)
44
+ if add.returncode != 0:
45
+ return False, (add.stderr or "git add failed").strip()
46
+ c = _git(repo, "commit", "-m", message, "--", *files)
47
+ if c.returncode != 0:
48
+ return False, (c.stderr or c.stdout or "git commit failed").strip().splitlines()[-1] \
49
+ if (c.stderr or c.stdout) else "git commit failed"
50
+ sha = _git(repo, "rev-parse", "--short", "HEAD")
51
+ return True, sha.stdout.strip() if sha.returncode == 0 else "committed"
52
+
53
+
54
+ def head_sha(repo: str) -> str:
55
+ return _git(repo, "rev-parse", "HEAD").stdout.strip()
56
+
57
+
58
+ def show(repo: str, commit: str, path: str) -> Optional[str]:
59
+ r = _git(repo, "show", f"{commit}:{path}")
60
+ return r.stdout if r.returncode == 0 else None
61
+
62
+
63
+ def diff_head(repo: str) -> str:
64
+ """Tracked modifications (staged + unstaged) vs HEAD, as text bytes for hashing."""
65
+ return _git(repo, "diff", "HEAD", "--no-color").stdout
66
+
67
+
68
+ def staged_diff(repo: str) -> str:
69
+ return _git(repo, "diff", "--cached", "--no-color").stdout
70
+
71
+
72
+ def commit_diff(repo: str, sha: str) -> str:
73
+ return _git(repo, "show", sha, "--no-color", "--format=").stdout
74
+
75
+
76
+ def untracked_files(repo: str) -> List[str]:
77
+ out = _git(repo, "status", "--porcelain", "-z").stdout
78
+ files = []
79
+ for entry in out.split("\0"):
80
+ if entry.startswith("?? "):
81
+ files.append(entry[3:])
82
+ return sorted(files)
83
+
84
+
85
+ def hash_object(repo: str, path: str) -> str:
86
+ """Content hash of a file WITHOUT storing it (no -w). Empty on failure."""
87
+ r = _git(repo, "hash-object", path)
88
+ return r.stdout.strip() if r.returncode == 0 else ""
89
+
90
+
91
+ def dirty_hash(repo: str) -> str:
92
+ """A deterministic digest of the working-tree delta vs HEAD: tracked diff + untracked
93
+ file hashes. 'clean' when there is nothing. No object-DB writes."""
94
+ import hashlib
95
+ tracked = diff_head(repo)
96
+ parts = [tracked]
97
+ for f in untracked_files(repo):
98
+ parts.append(f"{f}\0{hash_object(repo, f)}")
99
+ blob = "\n".join(parts)
100
+ if not blob.strip():
101
+ return "clean"
102
+ return hashlib.sha256(blob.encode("utf-8", "replace")).hexdigest()[:16]