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,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]