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
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""product_ops -- the verbs. Each returns a SubstratumResult. This is the ONLY module that
|
|
2
|
+
imports engine logic, and it does so lazily (heavy imports inside op_fix, after the first
|
|
3
|
+
progress event). Extracted from scripts/ci/substratum_fix_cli.py; the script becomes a shim.
|
|
4
|
+
|
|
5
|
+
Route 1: test-derived synthesis through the v0-synth gated door.
|
|
6
|
+
Route 2: the substrate registry (run_substrate_query), the one door.
|
|
7
|
+
Both arbitrated by the differential client gate. Nothing ships unverified.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from . import codes
|
|
17
|
+
from . import engine
|
|
18
|
+
from . import gitio
|
|
19
|
+
from . import refusals
|
|
20
|
+
from . import result as _result
|
|
21
|
+
from . import run_store
|
|
22
|
+
from . import runid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def op_fix_from_failing_test(repo: str, failing_tests: list[str], intent: str = "",
|
|
26
|
+
test_command: Optional[str] = None,
|
|
27
|
+
emit=None) -> "_result.SubstratumResult":
|
|
28
|
+
"""emit(stage, text): optional progress callback (GROUND/COMPOSE/GATE)."""
|
|
29
|
+
repo = os.path.abspath(repo)
|
|
30
|
+
def stage(s, t):
|
|
31
|
+
if emit:
|
|
32
|
+
emit(s, t)
|
|
33
|
+
base = gitio.head_sha(repo)
|
|
34
|
+
rid, key = runid.compute(repo, "fix",
|
|
35
|
+
{"failing_tests": sorted(failing_tests),
|
|
36
|
+
"intent_sha": _sha(intent), "test_cmd": test_command})
|
|
37
|
+
target = ", ".join(failing_tests)
|
|
38
|
+
inputs = {"failing_tests": list(failing_tests), "intent": intent,
|
|
39
|
+
"test_command": test_command}
|
|
40
|
+
|
|
41
|
+
stage("GROUND", f"reading {len(failing_tests)} failing test(s)")
|
|
42
|
+
|
|
43
|
+
if not engine.available(): # client-only install: fix needs the engine (local or hosted)
|
|
44
|
+
ref = refusals.build(codes.NO_ENGINE, engine_code="NO_ENGINE",
|
|
45
|
+
ctx={"target": target or "the failing test", "run_id": rid})
|
|
46
|
+
return _store(repo, _result.make(codes.REFUSED, rid, "fix", code=codes.NO_ENGINE, refusal=ref),
|
|
47
|
+
key, inputs)
|
|
48
|
+
|
|
49
|
+
# ---- ROUTE 1: test-derived synthesis ----
|
|
50
|
+
r1 = _route_test_derived(repo, base, failing_tests, intent, test_command, stage)
|
|
51
|
+
if r1 is not None:
|
|
52
|
+
patches, provenance, sink = r1
|
|
53
|
+
diff = "".join(_diff_for(repo, base, p, c) for p, c in sorted(patches.items()))
|
|
54
|
+
gate = _gate_from_sink(sink, failing_tests)
|
|
55
|
+
res = _result.make(codes.VERIFIED, rid, "fix", diff=diff, files=tuple(sorted(patches)),
|
|
56
|
+
provenance=_result.Provenance(entries=tuple(provenance)),
|
|
57
|
+
gate=gate, engine=_engine_info(),
|
|
58
|
+
data={"target": target})
|
|
59
|
+
return _store(repo, res, key, inputs)
|
|
60
|
+
|
|
61
|
+
# ---- ROUTE 2: substrate registry (the one door) ----
|
|
62
|
+
stage("COMPOSE", "querying the substrate registry")
|
|
63
|
+
h = engine.heavy()
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
trace: list = []
|
|
66
|
+
req = h["FixRequest"](buggy_source="", repo_provenance=os.path.basename(repo),
|
|
67
|
+
problem_statement=intent or "", clone_dir=Path(repo),
|
|
68
|
+
base_commit=base, fail_to_pass=tuple(failing_tests))
|
|
69
|
+
cand = h["run_substrate_query"](req, trace_out=trace)
|
|
70
|
+
if cand is None:
|
|
71
|
+
vbr = _verified_before_from_trace(trace)
|
|
72
|
+
ref = _refusal_from_trace(trace, target, rid, vbr)
|
|
73
|
+
res = _result.make(codes.REFUSED, rid, "fix", code=ref.code, refusal=ref,
|
|
74
|
+
engine=_engine_info(), data={"target": target})
|
|
75
|
+
return _store(repo, res, key, inputs)
|
|
76
|
+
|
|
77
|
+
patches = {}
|
|
78
|
+
if getattr(cand, "file_patches", None):
|
|
79
|
+
for fp in cand.file_patches:
|
|
80
|
+
patches[fp.path] = fp.patched_content
|
|
81
|
+
diff = "".join(_diff_for(repo, base, p, c) for p, c in sorted(patches.items()))
|
|
82
|
+
res = _result.make(codes.VERIFIED, rid, "fix", diff=diff, files=tuple(sorted(patches)),
|
|
83
|
+
provenance=_result.Provenance(entries=tuple(cand.provenance)[:10]),
|
|
84
|
+
engine=_engine_info(), data={"target": target})
|
|
85
|
+
return _store(repo, res, key)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# route 1 (ported verbatim from the CI script; emits progress; uses the sink)
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def _route_test_derived(repo, base_commit, failing_tests, intent, test_command, stage):
|
|
93
|
+
lib_root = engine.lib_roots.py_library_root()
|
|
94
|
+
if not lib_root:
|
|
95
|
+
return None
|
|
96
|
+
test_files = sorted({t.split("::")[0] for t in failing_tests if "::" in t or t.endswith(".py")})
|
|
97
|
+
leaf_names = {t.split("::")[-1].split("[")[0] for t in failing_tests if "::" in t}
|
|
98
|
+
for tf in test_files:
|
|
99
|
+
tf_abs = os.path.join(repo, tf)
|
|
100
|
+
if not os.path.isfile(tf_abs):
|
|
101
|
+
continue
|
|
102
|
+
tsrc = open(tf_abs, encoding="utf-8", errors="replace").read()
|
|
103
|
+
try:
|
|
104
|
+
tree = ast.parse(tsrc)
|
|
105
|
+
except SyntaxError:
|
|
106
|
+
continue
|
|
107
|
+
targets = []
|
|
108
|
+
for node in ast.walk(tree):
|
|
109
|
+
if isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
|
110
|
+
mod_rel = node.module.replace(".", "/") + ".py"
|
|
111
|
+
if os.path.isfile(os.path.join(repo, mod_rel)):
|
|
112
|
+
for a in node.names:
|
|
113
|
+
if re.search(rf"\b{re.escape(a.name)}\s*\(", tsrc):
|
|
114
|
+
targets.append((a.name, mod_rel))
|
|
115
|
+
for name, mod_rel in targets:
|
|
116
|
+
arities = [len(n.args) + len(n.keywords) for n in ast.walk(tree)
|
|
117
|
+
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
|
|
118
|
+
and n.func.id == name]
|
|
119
|
+
if not arities:
|
|
120
|
+
continue
|
|
121
|
+
arity = max(arities)
|
|
122
|
+
kw_names = [k.arg for n in ast.walk(tree) if isinstance(n, ast.Call)
|
|
123
|
+
and isinstance(n.func, ast.Name) and n.func.id == name
|
|
124
|
+
for k in n.keywords if k.arg]
|
|
125
|
+
pos = arity - len(set(kw_names))
|
|
126
|
+
params = [f"p{i}" for i in range(pos)] + list(dict.fromkeys(kw_names))
|
|
127
|
+
signature = f"def {name}({', '.join(params)}):"
|
|
128
|
+
from ._vendor._pro_test_derive import derive_gated_test as _dgt
|
|
129
|
+
gated = _dgt(_as_patch(tf, tsrc), name, sorted(leaf_names))
|
|
130
|
+
if not gated:
|
|
131
|
+
continue
|
|
132
|
+
stage("GROUND", f"test exercises {name} (from {mod_rel})")
|
|
133
|
+
h = engine.heavy()
|
|
134
|
+
lib = engine.load_library_cached(lib_root) # warm: loaded once per session
|
|
135
|
+
scn = {"id": f"cli:{name}", "intent": intent or "", "signature": signature,
|
|
136
|
+
"imports": [], "acceptance_test": gated}
|
|
137
|
+
stage("COMPOSE", f"synthesizing {name} from the library")
|
|
138
|
+
out = h["synthesize"](scn, lib, "gated")
|
|
139
|
+
if getattr(out, "outcome", "") != "EMIT" or not out.source:
|
|
140
|
+
continue
|
|
141
|
+
mod_abs = os.path.join(repo, mod_rel)
|
|
142
|
+
base_mod = open(mod_abs, encoding="utf-8", errors="replace").read()
|
|
143
|
+
new_mod = _splice(base_mod, name, out.source)
|
|
144
|
+
stage("GATE", f"differential gate on {', '.join(failing_tests)}")
|
|
145
|
+
sink: dict = {}
|
|
146
|
+
cg = engine.client_gate.run_client_gate(
|
|
147
|
+
repo, base_commit, {mod_rel: new_mod}, test_paths=list(failing_tests),
|
|
148
|
+
test_cmd=test_command, differential=True, transcript_sink=sink)
|
|
149
|
+
if cg.passed:
|
|
150
|
+
prov = [f"v0_synth:EMIT:{name}", "execution_gate:client:PASS"]
|
|
151
|
+
sel = getattr(out.provenance, "selected_pattern", None)
|
|
152
|
+
if sel is not None:
|
|
153
|
+
prov.insert(1, f"pattern:{getattr(sel, 'pattern_id', '?')}")
|
|
154
|
+
return {mod_rel: new_mod}, prov, sink
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def op_write_from_intent(repo: str, intent: str, signature: str,
|
|
159
|
+
into: Optional[str] = None, emit=None,
|
|
160
|
+
prefer_purpose: Optional[str] = None) -> "_result.SubstratumResult":
|
|
161
|
+
"""Greenfield from intent. Derivation, not generation: comprehend the intent, derive the
|
|
162
|
+
construction from the granular productions + grounding, verify intrinsically (construct-then-
|
|
163
|
+
verify, no external test needed), and EMIT or REFUSE. Same contract as everything else --
|
|
164
|
+
it resolves iff the intent is fully understood AND the construction is derivable; otherwise
|
|
165
|
+
it refuses by name. No LLM guess."""
|
|
166
|
+
repo = os.path.abspath(repo)
|
|
167
|
+
def stage(s, t):
|
|
168
|
+
if emit:
|
|
169
|
+
emit(s, t)
|
|
170
|
+
# hosted engine: when logged in, greenfield synthesis runs server-side (intent+sig only leave the
|
|
171
|
+
# machine) and returns the SAME SubstratumResult we render locally. Falls back to a clean refusal.
|
|
172
|
+
from . import remote
|
|
173
|
+
if remote.enabled():
|
|
174
|
+
stage("REMOTE", "hosted engine")
|
|
175
|
+
rid, key = runid.compute(repo, "write",
|
|
176
|
+
{"intent_sha": _sha(intent), "sig": signature, "into": into})
|
|
177
|
+
try:
|
|
178
|
+
return _result.from_json_dict(remote.write(intent, signature, into, prefer_purpose))
|
|
179
|
+
except remote.RemoteError as e:
|
|
180
|
+
ref = refusals.build(codes.GATE_PRECONDITION, engine_code=str(e),
|
|
181
|
+
ctx={"target": _name_from_sig(signature) or "the function",
|
|
182
|
+
"run_id": rid})
|
|
183
|
+
return _store(repo, _result.make(codes.REFUSED, rid, "write",
|
|
184
|
+
code=codes.GATE_PRECONDITION, refusal=ref), key,
|
|
185
|
+
{"intent": intent, "signature": signature})
|
|
186
|
+
if not engine.available():
|
|
187
|
+
rid, key = runid.compute(repo, "write",
|
|
188
|
+
{"intent_sha": _sha(intent), "sig": signature, "into": into})
|
|
189
|
+
ref = refusals.build(codes.NO_ENGINE, engine_code="NO_ENGINE",
|
|
190
|
+
ctx={"target": _name_from_sig(signature) or "the function", "run_id": rid})
|
|
191
|
+
return _store(repo, _result.make(codes.REFUSED, rid, "write", code=codes.NO_ENGINE, refusal=ref),
|
|
192
|
+
key, {"intent": intent, "signature": signature})
|
|
193
|
+
base = gitio.head_sha(repo)
|
|
194
|
+
name = _name_from_sig(signature)
|
|
195
|
+
sig = signature if signature.strip().startswith("def ") else f"def {signature.strip().rstrip(':')}:"
|
|
196
|
+
rid, key = runid.compute(repo, "write",
|
|
197
|
+
{"intent_sha": _sha(intent), "sig": sig, "into": into})
|
|
198
|
+
inputs = {"intent": intent, "signature": sig, "into": into}
|
|
199
|
+
target = name or "the requested function"
|
|
200
|
+
|
|
201
|
+
lib_root = engine.lib_roots.py_library_root()
|
|
202
|
+
if not lib_root:
|
|
203
|
+
ref = refusals.build(codes.NO_GROUNDED_CANDIDATE, engine_code="no_library",
|
|
204
|
+
ctx={"target": target, "run_id": rid})
|
|
205
|
+
return _store(repo, _result.make(codes.REFUSED, rid, "write", code=ref.code,
|
|
206
|
+
refusal=ref, data={"target": target}), key, inputs)
|
|
207
|
+
|
|
208
|
+
stage("GROUND", "comprehending intent")
|
|
209
|
+
h = engine.heavy()
|
|
210
|
+
lib = engine.load_library_cached(lib_root)
|
|
211
|
+
stage("COMPOSE", f"deriving {name or 'code'} from intent + productions")
|
|
212
|
+
if prefer_purpose: # accept a purpose_key OR the plain-English option text (--mean "...")
|
|
213
|
+
pp = prefer_purpose.strip().lower()
|
|
214
|
+
for o in _clarify_options(intent, sig, lib):
|
|
215
|
+
if o["purpose"].lower() == pp or o["text"].lower() == pp or pp in o["text"].lower():
|
|
216
|
+
prefer_purpose = o["purpose"]
|
|
217
|
+
break
|
|
218
|
+
_set_preferred_purpose(prefer_purpose) # a clarification pick forces that operation identity
|
|
219
|
+
try:
|
|
220
|
+
res = h["synthesize_from_intent"](intent, sig, library=lib)
|
|
221
|
+
finally:
|
|
222
|
+
_set_preferred_purpose(None)
|
|
223
|
+
|
|
224
|
+
if res.get("status") == "emit" and res.get("source"):
|
|
225
|
+
stage("VERIFY", "construct-then-verify (intrinsic, intent-derived)")
|
|
226
|
+
into_file = into or (f"{name}.py" if name else "substratum_new.py")
|
|
227
|
+
mod_abs = os.path.join(repo, into_file)
|
|
228
|
+
base_mod = open(mod_abs, encoding="utf-8", errors="replace").read() \
|
|
229
|
+
if os.path.isfile(mod_abs) else ""
|
|
230
|
+
new_mod = _splice(base_mod, name, res["source"])
|
|
231
|
+
diff = _diff_for(repo, base, into_file, new_mod)
|
|
232
|
+
prov = _prov_from_intent(res.get("provenance"), name)
|
|
233
|
+
note = "derived from intent and intrinsically verified (construct-then-verify); no external test."
|
|
234
|
+
out = _result.make(codes.VERIFIED, rid, "write", diff=diff, files=(into_file,),
|
|
235
|
+
provenance=_result.Provenance(entries=tuple(prov)),
|
|
236
|
+
engine=_engine_info(), data={"target": target, "note": note})
|
|
237
|
+
return _store(repo, out, key, inputs)
|
|
238
|
+
|
|
239
|
+
# ASK before refusing: if the intent maps to >=2 DISTINCT operations, surface a plain-English
|
|
240
|
+
# clarification (the inverse-gloss of each candidate) instead of a bare refusal. The user's pick
|
|
241
|
+
# forces that operation and the SAME pipeline re-resolves. Never an emission -> zero-wrong intact.
|
|
242
|
+
reason = (res.get("refuse_reason") or "no_candidate")
|
|
243
|
+
if prefer_purpose is None and _is_intent_ambiguous(reason):
|
|
244
|
+
opts = _clarify_options(intent, sig, lib)
|
|
245
|
+
if len(opts) >= 2:
|
|
246
|
+
ref = refusals.build(codes.UNDERDETERMINED_INTENT, engine_code=reason,
|
|
247
|
+
ctx={"target": target, "run_id": rid})
|
|
248
|
+
clarify = {"question": f"\"{intent}\" could mean a few things. Which did you mean?",
|
|
249
|
+
"options": opts, "intent": intent, "sig": sig, "into": into}
|
|
250
|
+
return _store(repo, _result.make(codes.UNVERIFIABLE, rid, "write",
|
|
251
|
+
code=codes.UNDERDETERMINED_INTENT, refusal=ref,
|
|
252
|
+
engine=_engine_info(),
|
|
253
|
+
data={"target": target, "clarify": clarify}), key, inputs)
|
|
254
|
+
|
|
255
|
+
# refuse: intent not fully understood, or no derivable construction (coverage)
|
|
256
|
+
code = codes.UNDERDETERMINED_INTENT if _is_intent_ambiguous(reason) else codes.NO_GROUNDED_CANDIDATE
|
|
257
|
+
vbr = (f"comprehended: intent parsed",) if code == codes.NO_GROUNDED_CANDIDATE else ()
|
|
258
|
+
ref = refusals.build(code, engine_code=reason, ctx={"target": target, "run_id": rid},
|
|
259
|
+
verified_before=vbr)
|
|
260
|
+
return _store(repo, _result.make(codes.REFUSED, rid, "write", code=code, refusal=ref,
|
|
261
|
+
engine=_engine_info(), data={"target": target}), key, inputs)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _readme_synopsis(repo: str) -> Optional[dict]:
|
|
265
|
+
"""Lift the project's self-description VERBATIM from its README (grounded, not generated): the
|
|
266
|
+
first H1 title + the first real prose paragraph, skipping badges / TOC / HTML / code. Returns
|
|
267
|
+
{title, blurb, source} or None. These are the AUTHORS' words, quoted and cited -- never a
|
|
268
|
+
Substratum paraphrase, so it cannot lie."""
|
|
269
|
+
import re
|
|
270
|
+
path = None
|
|
271
|
+
for name in ("README.md", "README.rst", "README.txt", "README", "readme.md",
|
|
272
|
+
"Readme.md", "README.markdown", os.path.join("docs", "README.md")):
|
|
273
|
+
p = os.path.join(repo, name)
|
|
274
|
+
if os.path.isfile(p):
|
|
275
|
+
path = p
|
|
276
|
+
break
|
|
277
|
+
if not path:
|
|
278
|
+
return None
|
|
279
|
+
try:
|
|
280
|
+
lines = open(path, encoding="utf-8", errors="replace").read().splitlines()
|
|
281
|
+
except Exception:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def clean(s):
|
|
285
|
+
s = re.sub(r"<[^>]+>", "", s) # strip HTML tags
|
|
286
|
+
s = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", s) # [text](url) -> text
|
|
287
|
+
return re.sub(r"\s+", " ", s.strip().lstrip("#").strip().strip("*_` ").strip())
|
|
288
|
+
|
|
289
|
+
def is_noise(line):
|
|
290
|
+
s = line.strip()
|
|
291
|
+
if not s:
|
|
292
|
+
return False
|
|
293
|
+
core = re.sub(r"!?\[[^\]]*\]\([^)]*\)|<[^>]+>|[\s|>*_=#`~-]", "", s)
|
|
294
|
+
return len(core) < 3 # only badges / rules / html / links
|
|
295
|
+
|
|
296
|
+
title = None
|
|
297
|
+
for l in lines:
|
|
298
|
+
m = re.match(r"^\s*#\s+(.+)$", l)
|
|
299
|
+
if m:
|
|
300
|
+
title = clean(m.group(1))
|
|
301
|
+
break
|
|
302
|
+
blurb, in_code, started = [], False, False
|
|
303
|
+
for l in lines:
|
|
304
|
+
s = l.strip()
|
|
305
|
+
if s.startswith("```") or s.startswith("~~~"):
|
|
306
|
+
in_code = not in_code
|
|
307
|
+
continue
|
|
308
|
+
if in_code:
|
|
309
|
+
continue
|
|
310
|
+
if re.match(r"^\s*#", l): # a heading ends the first paragraph
|
|
311
|
+
if started:
|
|
312
|
+
break
|
|
313
|
+
continue
|
|
314
|
+
if not s:
|
|
315
|
+
if started:
|
|
316
|
+
break
|
|
317
|
+
continue
|
|
318
|
+
if is_noise(l) or re.match(r"^\s*[-*]\s*\[", l): # badge row / TOC link
|
|
319
|
+
continue
|
|
320
|
+
c = clean(l)
|
|
321
|
+
if len(c) < 3:
|
|
322
|
+
continue
|
|
323
|
+
blurb.append(c)
|
|
324
|
+
started = True
|
|
325
|
+
text = re.sub(r"\s+", " ", " ".join(blurb)).strip()
|
|
326
|
+
if len(text) > 360:
|
|
327
|
+
text = text[:360].rsplit(" ", 1)[0] + "…"
|
|
328
|
+
if not title and not text:
|
|
329
|
+
return None
|
|
330
|
+
return {"title": title or "", "blurb": text, "source": os.path.relpath(path, repo)}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _project_synopsis(repo: str) -> Optional[dict]:
|
|
334
|
+
"""Gather EVERY grounded description of the project -- curated package manifests (which are
|
|
335
|
+
usually more current than a README) + the README + (upstream) the real code structure. Each
|
|
336
|
+
field is quoted from a real file and CITED; nothing is paraphrased, so it cannot lie. READMEs
|
|
337
|
+
go stale, so the manifest tagline is preferred for the one-liner and both are shown when they
|
|
338
|
+
differ -- the developer sees all grounded context and reconciles against the code counts."""
|
|
339
|
+
import json as _json
|
|
340
|
+
name = tagline = None
|
|
341
|
+
sources: list = []
|
|
342
|
+
|
|
343
|
+
def _read(rel):
|
|
344
|
+
p = os.path.join(repo, rel)
|
|
345
|
+
try:
|
|
346
|
+
return open(p, encoding="utf-8", errors="replace").read() if os.path.isfile(p) else None
|
|
347
|
+
except Exception:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
pj = _read("package.json")
|
|
351
|
+
if pj:
|
|
352
|
+
try:
|
|
353
|
+
d = _json.loads(pj)
|
|
354
|
+
if d.get("name") or d.get("description"):
|
|
355
|
+
name = name or (d.get("name") or "").strip() or None
|
|
356
|
+
tagline = tagline or (d.get("description") or "").strip() or None
|
|
357
|
+
sources.append("package.json")
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
for man in ("pyproject.toml", "Cargo.toml"):
|
|
361
|
+
raw = _read(man)
|
|
362
|
+
if not raw:
|
|
363
|
+
continue
|
|
364
|
+
try:
|
|
365
|
+
import tomllib
|
|
366
|
+
t = tomllib.loads(raw)
|
|
367
|
+
except Exception:
|
|
368
|
+
t = None
|
|
369
|
+
desc = nm = None
|
|
370
|
+
if isinstance(t, dict):
|
|
371
|
+
proj = t.get("project") or (t.get("tool", {}).get("poetry") if man == "pyproject.toml" else None) \
|
|
372
|
+
or t.get("package") or {}
|
|
373
|
+
if isinstance(proj, dict):
|
|
374
|
+
nm = (proj.get("name") or "").strip() or None
|
|
375
|
+
desc = (proj.get("description") or "").strip() or None
|
|
376
|
+
if desc or nm:
|
|
377
|
+
name = name or nm
|
|
378
|
+
tagline = tagline or desc
|
|
379
|
+
sources.append(man)
|
|
380
|
+
gomod = _read("go.mod")
|
|
381
|
+
if gomod:
|
|
382
|
+
m = _re_first(r"^module\s+(\S+)", gomod)
|
|
383
|
+
if m and not name:
|
|
384
|
+
name = m.rsplit("/", 1)[-1]
|
|
385
|
+
sources.append("go.mod")
|
|
386
|
+
|
|
387
|
+
rd = _readme_synopsis(repo)
|
|
388
|
+
blurb = None
|
|
389
|
+
if rd:
|
|
390
|
+
name = name or rd.get("title") or None
|
|
391
|
+
blurb = rd.get("blurb") or None
|
|
392
|
+
if rd.get("source"):
|
|
393
|
+
sources.append(rd["source"])
|
|
394
|
+
if not tagline: # no manifest one-liner -> the README blurb is the tagline
|
|
395
|
+
tagline, blurb = blurb, None
|
|
396
|
+
|
|
397
|
+
if not (name or tagline or blurb):
|
|
398
|
+
return None
|
|
399
|
+
# if the README blurb just restates the tagline, don't show it twice
|
|
400
|
+
if blurb and tagline and blurb[:40].lower() in tagline.lower():
|
|
401
|
+
blurb = None
|
|
402
|
+
return {"name": name or "", "tagline": tagline or "", "blurb": blurb or "",
|
|
403
|
+
"sources": sorted(set(sources))}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _re_first(pat: str, text: str):
|
|
407
|
+
import re
|
|
408
|
+
m = re.search(pat, text, re.M)
|
|
409
|
+
return m.group(1) if m else None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _humanize_token(name: str) -> str:
|
|
413
|
+
"""Stdlib humanizer (camelCase/snake_case -> words) so summarize stays fully local."""
|
|
414
|
+
import re as _re
|
|
415
|
+
s = _re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", str(name or ""))
|
|
416
|
+
return s.replace("_", " ").strip() or str(name or "")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _local_gloss_fns():
|
|
420
|
+
"""Prefer the engine's rich inverse-gloss; fall back to the stdlib humanizer when the engine
|
|
421
|
+
is not installed, so `summarize` runs on a zero-dependency (client-only) install."""
|
|
422
|
+
try:
|
|
423
|
+
engine._ensure_repo_on_path()
|
|
424
|
+
from backend.substratum_v2.code_generation_v2.intent_gloss import gloss_name, humanize_name
|
|
425
|
+
return gloss_name, humanize_name
|
|
426
|
+
except Exception:
|
|
427
|
+
return _humanize_token, _humanize_token
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def op_summarize_repo(repo: str, emit=None) -> "_result.SubstratumResult":
|
|
431
|
+
"""Describe a codebase in plain English across EVERY serviced language (Python/Go/TS/JS), using
|
|
432
|
+
the SAME inverse-gloss the clarifier uses. Stdlib-only (langscan: ast for Python, closed-class
|
|
433
|
+
declaration regexes for the rest), no library load. Grounded: every line names a real symbol at
|
|
434
|
+
a real path. Comprehension surface, not generation -- no gaps by language."""
|
|
435
|
+
from . import langscan
|
|
436
|
+
repo = os.path.abspath(repo)
|
|
437
|
+
rid, key = runid.compute(repo, "summarize", {})
|
|
438
|
+
if emit:
|
|
439
|
+
emit("READ", "scanning modules across languages")
|
|
440
|
+
gloss_name, humanize_name = _local_gloss_fns()
|
|
441
|
+
|
|
442
|
+
_TYPE_KINDS = ("class", "struct", "interface", "type", "enum")
|
|
443
|
+
_CAP = 24
|
|
444
|
+
funcs: list = []
|
|
445
|
+
types: list = []
|
|
446
|
+
by_lang: dict = {} # lang -> {files, function, method, class, struct, interface, type, enum}
|
|
447
|
+
total_files = 0
|
|
448
|
+
for root, dirs, fs in os.walk(repo):
|
|
449
|
+
dirs[:] = [d for d in dirs if d not in
|
|
450
|
+
(".git", "node_modules", ".venv", "venv", "__pycache__", ".substratum", "dist", "build")]
|
|
451
|
+
for f in sorted(fs):
|
|
452
|
+
ext = os.path.splitext(f)[1]
|
|
453
|
+
lang = langscan.LANGS.get(ext)
|
|
454
|
+
if not lang:
|
|
455
|
+
continue
|
|
456
|
+
total_files += 1
|
|
457
|
+
rel = os.path.relpath(os.path.join(root, f), repo)
|
|
458
|
+
try:
|
|
459
|
+
text = open(os.path.join(root, f), encoding="utf-8", errors="replace").read()
|
|
460
|
+
except Exception:
|
|
461
|
+
continue
|
|
462
|
+
lstat = by_lang.setdefault(lang, {"files": 0})
|
|
463
|
+
lstat["files"] += 1
|
|
464
|
+
ff = ft = 0 # per-file caps so the described lists stay DIVERSE across the codebase
|
|
465
|
+
for d in langscan.scan(text, ext):
|
|
466
|
+
k, nm = d["kind"], d["name"]
|
|
467
|
+
lstat[k] = lstat.get(k, 0) + 1
|
|
468
|
+
if nm.startswith("_") or nm.startswith("$"):
|
|
469
|
+
continue
|
|
470
|
+
if k == "function" and len(funcs) < _CAP and ff < 4:
|
|
471
|
+
funcs.append({"symbol": nm, "file": rel, "lang": lang,
|
|
472
|
+
"english": gloss_name(nm)}); ff += 1
|
|
473
|
+
elif k in _TYPE_KINDS and len(types) < _CAP and ft < 4:
|
|
474
|
+
types.append({"symbol": nm, "file": rel, "lang": lang, "kind": k,
|
|
475
|
+
"english": humanize_name(nm)}); ft += 1
|
|
476
|
+
if total_files >= 4000: # backstop for pathological monorepos; disclosed if hit
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
n_func = sum(l.get("function", 0) for l in by_lang.values())
|
|
480
|
+
n_meth = sum(l.get("method", 0) for l in by_lang.values())
|
|
481
|
+
n_type = sum(l.get(k, 0) for l in by_lang.values() for k in _TYPE_KINDS)
|
|
482
|
+
summary = {
|
|
483
|
+
"about": _project_synopsis(repo), # all grounded sources: manifests + README, cited
|
|
484
|
+
"total_code_files": total_files,
|
|
485
|
+
"languages": {lang: l["files"] for lang, l in by_lang.items()},
|
|
486
|
+
"functions": n_func, "methods": n_meth, "types": n_type,
|
|
487
|
+
"functions_shown": len(funcs), "functions_truncated": max(0, n_func - len(funcs)),
|
|
488
|
+
"described": funcs, "types_described": types,
|
|
489
|
+
"multi_lang": len([l for l in by_lang if by_lang[l]["files"]]) > 1,
|
|
490
|
+
"truncated_files": total_files >= 4000,
|
|
491
|
+
}
|
|
492
|
+
res = _result.make(codes.PASS, rid, "summarize", data={"summary": summary}, engine=_engine_info())
|
|
493
|
+
return _store(repo, res, key, {"repo": repo})
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _set_preferred_purpose(purpose: Optional[str]) -> None:
|
|
497
|
+
"""Channel a clarification pick to SELECT: the user's chosen operation is the disambiguation,
|
|
498
|
+
so intent_alone forces the survivor whose purpose matches it. Cleared after the call."""
|
|
499
|
+
try:
|
|
500
|
+
engine._ensure_repo_on_path()
|
|
501
|
+
from backend.substratum_v2.code_generation_v2 import v0_select as _vs
|
|
502
|
+
_vs._PREFERRED_PURPOSE = (purpose or "").strip() or None
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _clarify_options(intent: str, signature: str, lib) -> list:
|
|
508
|
+
"""The candidate operations for an ambiguous intent, each glossed to plain English (the inverse
|
|
509
|
+
of the parse) and deduped by that English so noise collapses. Reuses the engine helpers."""
|
|
510
|
+
try:
|
|
511
|
+
engine._ensure_repo_on_path()
|
|
512
|
+
from backend.substratum_v2.code_generation_v2.v0_intent_parser import parse_intent
|
|
513
|
+
from backend.substratum_v2.code_generation_v2.v0_select import _identity_survivors
|
|
514
|
+
from backend.substratum_v2.code_generation_v2.intent_gloss import gloss
|
|
515
|
+
except Exception:
|
|
516
|
+
return []
|
|
517
|
+
scn = {"id": "cli", "intent": intent, "signature": signature, "acceptance_test": ""}
|
|
518
|
+
try:
|
|
519
|
+
q, _ref = parse_intent(scn, "intent_alone")
|
|
520
|
+
except Exception:
|
|
521
|
+
return []
|
|
522
|
+
if q is None:
|
|
523
|
+
return []
|
|
524
|
+
seen: dict = {}
|
|
525
|
+
for v in _identity_survivors(q, lib):
|
|
526
|
+
pk = (getattr(v, "purpose_key", "") or "").split()
|
|
527
|
+
verb = pk[0] if pk else ""
|
|
528
|
+
obj = pk[1] if len(pk) > 1 else ""
|
|
529
|
+
eng = gloss(verb, obj, getattr(v, "category", ""))
|
|
530
|
+
if eng and eng not in seen:
|
|
531
|
+
seen[eng] = {"text": eng, "refine": eng.lower(),
|
|
532
|
+
"purpose": getattr(v, "purpose_key", "")}
|
|
533
|
+
return list(seen.values())[:5]
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _name_from_sig(signature: str) -> str:
|
|
537
|
+
import re as _re
|
|
538
|
+
m = _re.search(r"(?:def\s+)?([A-Za-z_]\w*)\s*\(", signature or "")
|
|
539
|
+
return m.group(1) if m else ""
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _is_intent_ambiguous(reason: str) -> bool:
|
|
543
|
+
r = (reason or "").upper()
|
|
544
|
+
return any(k in r for k in ("AMBIG", "UNDERDETERMIN", "MULTIPLE", "SELECT_AMBIGUOUS"))
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _prov_from_intent(prov, name: str) -> list:
|
|
548
|
+
entries = [f"v0_synth:intent_alone:EMIT{':' + name if name else ''}"]
|
|
549
|
+
try:
|
|
550
|
+
if isinstance(prov, dict):
|
|
551
|
+
pid = (prov.get("selected_pattern") or {}).get("pattern_id") if isinstance(
|
|
552
|
+
prov.get("selected_pattern"), dict) else prov.get("pattern_id")
|
|
553
|
+
if pid:
|
|
554
|
+
entries.append(f"pattern:{pid}")
|
|
555
|
+
except Exception:
|
|
556
|
+
pass
|
|
557
|
+
entries.append("verify:intrinsic:PASS")
|
|
558
|
+
return entries
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _as_patch(path: str, content: str) -> str:
|
|
562
|
+
lines = "\n".join("+" + l for l in content.splitlines())
|
|
563
|
+
return f"+++ b/{path}\n{lines}\n"
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _splice(module_src: str, fn_name: str, emitted: str) -> str:
|
|
567
|
+
try:
|
|
568
|
+
tree = ast.parse(module_src)
|
|
569
|
+
except SyntaxError:
|
|
570
|
+
return module_src + "\n\n" + emitted + "\n"
|
|
571
|
+
for node in tree.body:
|
|
572
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == fn_name:
|
|
573
|
+
lines = module_src.splitlines()
|
|
574
|
+
start = node.lineno - 1
|
|
575
|
+
if node.decorator_list:
|
|
576
|
+
start = node.decorator_list[0].lineno - 1
|
|
577
|
+
end = node.end_lineno
|
|
578
|
+
return "\n".join(lines[:start] + emitted.splitlines() + lines[end:]) + "\n"
|
|
579
|
+
return module_src.rstrip("\n") + "\n\n\n" + emitted.rstrip("\n") + "\n"
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _diff_for(repo: str, base_commit: str, path: str, new_content: str) -> str:
|
|
583
|
+
base = gitio.show(repo, base_commit, path) or ""
|
|
584
|
+
return engine.canonical_diff()(path, base, new_content)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
# refusal derivation from the substrate trace
|
|
589
|
+
# ---------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
def _verified_before_from_trace(trace) -> tuple:
|
|
592
|
+
composed = sum(1 for t in trace if getattr(t, "status", "") in
|
|
593
|
+
("candidate_failed_verify", "candidate_failed_execution_gate"))
|
|
594
|
+
lines = []
|
|
595
|
+
if composed:
|
|
596
|
+
lines.append(f"composed: {composed} candidate(s), all failed the gate")
|
|
597
|
+
no_cand = sum(1 for t in trace if getattr(t, "status", "") == "no_candidate")
|
|
598
|
+
if no_cand and not composed:
|
|
599
|
+
lines.append("grounded: no candidate matched (coverage absent)")
|
|
600
|
+
return tuple(lines)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _refusal_from_trace(trace, target, rid, vbr):
|
|
604
|
+
statuses = [getattr(t, "status", "") for t in trace]
|
|
605
|
+
if any(s in ("candidate_failed_verify", "candidate_failed_execution_gate") for s in statuses):
|
|
606
|
+
eng = "candidate_failed_verify"
|
|
607
|
+
else:
|
|
608
|
+
eng = "no_candidate"
|
|
609
|
+
return refusals.from_engine(eng, ctx={"target": target, "run_id": rid},
|
|
610
|
+
verified_before=vbr)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _gate_from_sink(sink: dict, tests) -> "_result.GateTranscript":
|
|
614
|
+
base_rc = sink.get("base_rc")
|
|
615
|
+
patched_rc = sink.get("patched_rc")
|
|
616
|
+
return _result.GateTranscript(
|
|
617
|
+
kind="client_differential", test_ids=tuple(tests),
|
|
618
|
+
runner_command=tuple(sink.get("runner_command", ())),
|
|
619
|
+
base_status="FAIL" if (base_rc not in (None, 0)) else ("PASS" if base_rc == 0 else ""),
|
|
620
|
+
patched_status="PASS" if patched_rc == 0 else ("FAIL" if patched_rc is not None else ""),
|
|
621
|
+
base_excerpt=(sink.get("base_out", "") or "")[-800:],
|
|
622
|
+
patched_excerpt=(sink.get("patched_out", "") or "")[-800:],
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _engine_info() -> "_result.EngineInfo":
|
|
627
|
+
return _result.EngineInfo(engine_version=runid.engine_version(),
|
|
628
|
+
library_versions=tuple(f"{k}:{v}" for k, v in
|
|
629
|
+
runid.library_versions().items()))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _store(repo, res, key, inputs=None):
|
|
633
|
+
run_store.store_run(repo, res, run_key=key, inputs=inputs)
|
|
634
|
+
return res
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _sha(text: str) -> str:
|
|
638
|
+
import hashlib
|
|
639
|
+
return hashlib.sha256((text or "").encode("utf-8", "replace")).hexdigest()[:16]
|