substratum-cli 0.2.8__tar.gz → 0.2.9__tar.gz

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 (120) hide show
  1. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/PKG-INFO +1 -1
  2. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/__init__.py +1 -1
  3. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_boot.py +2 -3
  4. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/auth.py +8 -0
  5. substratum_cli-0.2.9/substratum_cli/commands/where.py +137 -0
  6. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/render.py +22 -1
  7. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/session.py +104 -2
  8. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/PKG-INFO +1 -1
  9. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/SOURCES.txt +1 -0
  10. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/README.md +0 -0
  11. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/pyproject.toml +0 -0
  12. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/setup.cfg +0 -0
  13. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/README.md +0 -0
  14. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/__main__.py +0 -0
  15. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_tui.py +0 -0
  16. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_update.py +0 -0
  17. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_vendor/__init__.py +0 -0
  18. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_vendor/_pro_test_derive.py +0 -0
  19. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_vendor/client_gate.py +0 -0
  20. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_vendor/css_gate.py +0 -0
  21. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/_wordmark.py +0 -0
  22. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/account.py +0 -0
  23. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/build_state.py +0 -0
  24. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/cli.py +0 -0
  25. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/codes.py +0 -0
  26. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/__init__.py +0 -0
  27. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/apply.py +0 -0
  28. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/commit.py +0 -0
  29. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/css_fix.py +0 -0
  30. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/explain.py +0 -0
  31. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/failing.py +0 -0
  32. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/fix.py +0 -0
  33. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/history.py +0 -0
  34. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/init_cmd.py +0 -0
  35. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/pr.py +0 -0
  36. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/recall.py +0 -0
  37. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/recheck.py +0 -0
  38. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/replay.py +0 -0
  39. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/rules.py +0 -0
  40. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/scaffold.py +0 -0
  41. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/show.py +0 -0
  42. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/undo.py +0 -0
  43. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/verify.py +0 -0
  44. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/commands/write.py +0 -0
  45. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/config.py +0 -0
  46. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/diffs.py +0 -0
  47. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/engine.py +0 -0
  48. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/gitio.py +0 -0
  49. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/identity.py +0 -0
  50. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/langscan.py +0 -0
  51. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/product_ops.py +0 -0
  52. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/refusals.py +0 -0
  53. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/remote.py +0 -0
  54. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/result.py +0 -0
  55. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/rulebook.py +0 -0
  56. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/run_store.py +0 -0
  57. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/runid.py +0 -0
  58. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli/style.py +0 -0
  59. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/dependency_links.txt +0 -0
  60. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/entry_points.txt +0 -0
  61. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/requires.txt +0 -0
  62. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/substratum_cli.egg-info/top_level.txt +0 -0
  63. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_adaptive_prior.py +0 -0
  64. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_alias_warrant_trace.py +0 -0
  65. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_bidirectional_gate_contracts.py +0 -0
  66. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_coherence_evolution.py +0 -0
  67. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_coherences.py +0 -0
  68. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_connection_web_api.py +0 -0
  69. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_cross_entity_scoping.py +0 -0
  70. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_data.py +0 -0
  71. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_de28_negation_refuse.py +0 -0
  72. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_definite_description_handler.py +0 -0
  73. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_demo_regression_catalog.py +0 -0
  74. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_demo_response_shape.py +0 -0
  75. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_e1_e2_field_alignment.py +0 -0
  76. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_extraction_class_write_time.py +0 -0
  77. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_falsification_predictions.py +0 -0
  78. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_gap_d_wh_normalization.py +0 -0
  79. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_inference.py +0 -0
  80. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_inflection_cvc_rule.py +0 -0
  81. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_inflection_irregular.py +0 -0
  82. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_intervention.py +0 -0
  83. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_invariant.py +0 -0
  84. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_legal_citation_handler.py +0 -0
  85. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_lens_noop_seam.py +0 -0
  86. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_markup_tag_handler.py +0 -0
  87. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_nl_parser.py +0 -0
  88. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_nl_parser_adversarial.py +0 -0
  89. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_noise.py +0 -0
  90. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_pass2_contextual.py +0 -0
  91. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_perspectives.py +0 -0
  92. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_polysemous_token_alias_guard.py +0 -0
  93. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_possessive_normalization.py +0 -0
  94. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_predicate_canon.py +0 -0
  95. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_projection_gate.py +0 -0
  96. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_projection_gate_tag_question.py +0 -0
  97. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_query_normalizer.py +0 -0
  98. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_records_answer_format.py +0 -0
  99. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_referent_aboutness_gate.py +0 -0
  100. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_referent_quality_gate.py +0 -0
  101. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_refuse_message_topic_neutrality.py +0 -0
  102. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_relation_detection.py +0 -0
  103. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_relaxation.py +0 -0
  104. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_residual_content_gate.py +0 -0
  105. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_reverse_lookup_subject_filter.py +0 -0
  106. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_sec_statement_aliases.py +0 -0
  107. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_span_lens.py +0 -0
  108. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_span_lens_query_integration.py +0 -0
  109. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_substrate.py +0 -0
  110. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_synonymy_detector.py +0 -0
  111. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_table_ingest.py +0 -0
  112. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_triple_ingest.py +0 -0
  113. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_two_phase_schema.py +0 -0
  114. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_user_asserted_aliases.py +0 -0
  115. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_v2_api.py +0 -0
  116. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_v2_api_claims.py +0 -0
  117. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_v2_claim_determinism.py +0 -0
  118. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_word_role.py +0 -0
  119. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_word_role_wiring.py +0 -0
  120. {substratum_cli-0.2.8 → substratum_cli-0.2.9}/tests/test_xbrl_doc_aliases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: substratum-cli
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Substratum: verified-or-refused code, with provenance. The terminal over the comprehension engine.
5
5
  Author: Substratum
6
6
  License: LicenseRef-Proprietary
@@ -5,4 +5,4 @@ Import budget: this package is stdlib-only; the ONLY module reaching into backen
5
5
  """
6
6
  # SINGLE SOURCE OF TRUTH for the release version. pyproject reads this (dynamic version), the boot shows
7
7
  # it, and the update-check compares against it. To cut a release, bump ONLY this line, then publish.
8
- __version__ = "0.2.8"
8
+ __version__ = "0.2.9"
@@ -86,9 +86,8 @@ def render(name, census, langs, substrate, substrate_note, substrate_ok, standin
86
86
 
87
87
  # ---- WORDMARK + tagline ----
88
88
  wm = " ".join("SUBSTRATUM")
89
- tag = "proof-carrying code engine" + c(CH, " · ") + "zero-LLM" + c(CH, " · ") + "deterministic"
90
- out += ["", " " + c(L + B, wm) + " " + c(LAB, "proof-carrying code engine")
91
- + c(CH, f" {dot} ") + c(LAB, "zero-LLM") + c(CH, f" {dot} ") + c(LAB, "deterministic"), ""]
89
+ out += ["", " " + c(L + B, wm) + " " + c(LAB, "Proof-Carrying Code Engine")
90
+ + c(CH, f" {dot} ") + c(LAB, "Zero-LLM") + c(CH, f" {dot} ") + c(LAB, "Deterministic"), ""]
92
91
 
93
92
  # ---- the CERTIFICATE ----
94
93
  tl, tr, bl, br, h_, v_, ml, mr = ("+", "+", "+", "+", "-", "|", "+", "+") if ascii_mode \
@@ -1,6 +1,7 @@
1
1
  """`login` / `logout` / `whoami` -- connect the thin client to the hosted engine via GitHub."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import sys
5
6
 
6
7
  from .. import account, remote
@@ -23,7 +24,14 @@ def cmd_login() -> int:
23
24
 
24
25
 
25
26
  def cmd_logout() -> int:
27
+ if not account.is_logged_in():
28
+ sys.stdout.write(" not logged in -- nothing to disconnect.\n")
29
+ return 0
26
30
  account.clear()
31
+ if os.environ.get("SUBSTRATUM_TOKEN"):
32
+ sys.stdout.write(" saved login cleared, but SUBSTRATUM_TOKEN is set in this shell and "
33
+ "still authenticates -- unset it to fully disconnect.\n")
34
+ return 0
27
35
  sys.stdout.write(" logged out (local commands still work offline).\n")
28
36
  return 0
29
37
 
@@ -0,0 +1,137 @@
1
+ """`where <words>` -- ground plain-English domain terms against THIS repo: which files and lines
2
+ carry the feature the user named. The answer to "can we remove the spending check option from the
3
+ workflows section?" is never "not a command": it is WHERE that feature lives, plus the honest
4
+ ladder to a verified change (scaffold-test pins the behavior -> fix derives against it).
5
+
6
+ Deterministic and engine-free: term extraction is closed-class (stopwords out, identifier variants
7
+ generated -- snake/camel/pascal/kebab/joined), the search is `git grep` at the working tree, and
8
+ ranking is by variant specificity (a compound identifier hit outweighs a bare word). Results are a
9
+ first-class stored run.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ import subprocess
16
+
17
+ from .. import codes, run_store, runid
18
+ from .. import result as _result
19
+
20
+ _STOP = frozenset("""a an the can could we you i it is are was be do does did please help want
21
+ need from in on of to for and or with our my your this that these those app application website
22
+ site page section sections option options button tab menu part area remove delete disable enable
23
+ add create rename move change modify update refactor hide show make let lets get put set fix
24
+ """.split())
25
+
26
+ _MAX_FILES = 8
27
+ _MAX_HITS_PER_FILE = 3
28
+
29
+
30
+ def extract_terms(text: str) -> list:
31
+ """Content words, in order, stopwords and non-words dropped."""
32
+ words = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", (text or "").lower())
33
+ return [w for w in words if w not in _STOP]
34
+
35
+
36
+ def identifier_variants(terms: list) -> list:
37
+ """The code shapes adjacent term pairs (and singles) take: spending+check ->
38
+ spending_check / spendingCheck / SpendingCheck / spending-check / spendingcheck.
39
+ Compounds first (highest specificity), then the singles."""
40
+ out, seen = [], set()
41
+
42
+ def add(v, weight):
43
+ if v.lower() not in seen:
44
+ seen.add(v.lower())
45
+ out.append((v, weight))
46
+
47
+ for a, b in zip(terms, terms[1:]):
48
+ add(f"{a}_{b}", 5)
49
+ add(f"{a}{b[:1].upper()}{b[1:]}", 5)
50
+ add(f"{a[:1].upper()}{a[1:]}{b[:1].upper()}{b[1:]}", 5)
51
+ add(f"{a}-{b}", 5)
52
+ add(f"{a} {b}", 4)
53
+ for t in terms:
54
+ add(t, 1)
55
+ return out
56
+
57
+
58
+ def _git_grep(repo: str, needle: str) -> list:
59
+ """[(path, line_no, line_text)] for a fixed-string, case-insensitive working-tree grep."""
60
+ r = subprocess.run(["git", "grep", "-I", "-i", "-n", "-F", needle],
61
+ cwd=repo, capture_output=True, text=True, errors="replace", timeout=30)
62
+ hits = []
63
+ for ln in r.stdout.splitlines()[:400]:
64
+ parts = ln.split(":", 2)
65
+ if len(parts) == 3 and parts[1].isdigit():
66
+ if len(parts[2]) > 500: # a line that long is a data blob or minified, not code
67
+ continue
68
+ hits.append((parts[0], int(parts[1]), parts[2].strip()[:100]))
69
+ return hits
70
+
71
+
72
+ def ground_terms(repo: str, text: str) -> dict:
73
+ """The grounding: variants -> git grep -> files ranked by RARITY-WEIGHTED presence. A variant
74
+ hitting 3 files is a signal; one hitting 80 files ('check') is noise -- its weight is divided
75
+ by its spread. Presence counts once per (file, variant); sample lines come from the strongest
76
+ variant that hit the file, so the display shows the most specific evidence."""
77
+ terms = extract_terms(text)
78
+ variants = identifier_variants(terms)
79
+ per_variant: dict = {}
80
+ matched_variants: list = []
81
+ for needle, weight in variants:
82
+ hits = _git_grep(repo, needle)
83
+ if not hits:
84
+ continue
85
+ matched_variants.append(needle)
86
+ by_file: dict = {}
87
+ for path, line_no, line in hits:
88
+ by_file.setdefault(path, []).append({"line": line_no, "text": line, "term": needle})
89
+ per_variant[needle] = (weight, by_file)
90
+ file_score: dict = {}
91
+ file_best: dict = {} # path -> (effective weight of its strongest variant, samples)
92
+ for needle, (weight, by_file) in per_variant.items():
93
+ spread = len(by_file)
94
+ eff = weight if spread <= 3 else weight * 3.0 / spread # rarity: spread dilutes weight
95
+ for path, samples in by_file.items():
96
+ file_score[path] = file_score.get(path, 0.0) + eff
97
+ if path not in file_best or eff > file_best[path][0]:
98
+ file_best[path] = (eff, samples[:_MAX_HITS_PER_FILE])
99
+ ranked = sorted(file_score.items(), key=lambda kv: -kv[1])[:_MAX_FILES]
100
+ if ranked: # a file at <5% of the leader is a substring accident, not a place
101
+ floor = ranked[0][1] / 20.0
102
+ ranked = [(p, s) for p, s in ranked if s >= floor]
103
+ return {"terms": terms, "variants_matched": matched_variants,
104
+ "files": [{"path": p, "score": round(s, 2), "hits": file_best[p][1]}
105
+ for p, s in ranked]}
106
+
107
+
108
+ def next_steps(files: list) -> list:
109
+ """The engineer's honest path from here. `open <n>` is the concrete move that works today: go to
110
+ the strongest match and make the change. The PROOF path is stated truthfully -- `verify` gates an
111
+ edit only against a test that pins the intended behavior (a bare verify on a hand edit reports,
112
+ it never proves), so the warrant step is named, not skipped. No `commit` here: commit follows a
113
+ PASS, and nothing is proven yet."""
114
+ if not files:
115
+ return []
116
+ top = files[0]
117
+ hits = top.get("hits") or []
118
+ name = top["path"].rsplit("/", 1)[-1]
119
+ loc = f"{name}:{hits[0]['line']}" if hits else name
120
+ return [
121
+ {"cmd": "open 1", "note": f"the strongest match ({loc}); open it and make the change"},
122
+ {"cmd": "verify --test <t>",
123
+ "note": "prove the edit against a test that pins the behavior (scaffold-test writes one)"},
124
+ ]
125
+
126
+
127
+ def cmd_where(repo: str, text: str) -> "_result.SubstratumResult":
128
+ repo = os.path.abspath(repo)
129
+ rid, key = runid.compute(repo, "where", {"text": " ".join(extract_terms(text))})
130
+ g = ground_terms(repo, text)
131
+ res = _result.make(codes.PASS, rid, "where",
132
+ data={"query": text, **g,
133
+ "next_steps": next_steps(g["files"]),
134
+ "ladder": ("scaffold-test pins the wanted behavior as a failing "
135
+ "test; fix then derives a verified change against it")})
136
+ run_store.store_run(repo, res, run_key=key)
137
+ return res
@@ -347,6 +347,27 @@ def render_human(res: SubstratumResult, stream=None, color: str | None = None) -
347
347
  lab_col=(S.verdict_color(codes.FAIL) if st == "regressed" else None))
348
348
  out.append(p(S.g("foot") + S.g("bar") * 2, vcol))
349
349
 
350
+ # ---- where: domain terms grounded in this repo ----
351
+ if res.operation == "where" and res.data:
352
+ d = res.data
353
+ files = d.get("files") or []
354
+ body("WHERE", f"{len(files)} place(s) carry {', '.join(d.get('variants_matched') or d.get('terms') or [])[:60]}"
355
+ if files else f"nothing in this repo matches {', '.join(d.get('terms') or [])[:60]}")
356
+ for i, f in enumerate(files, 1):
357
+ out.append(f"{rail} {p(str(i).rjust(2), S.CHROME)} {p(f['path'], S.PROVEN)}")
358
+ for h in f.get("hits", [])[:2]:
359
+ out.append(f"{rail} {p('L' + str(h['line']) + ' ' + h['text'][:68], S.CHROME)}")
360
+ out.append(p(S.g("foot") + S.g("bar") * 2, vcol))
361
+ if files:
362
+ steps = d.get("next_steps") or []
363
+ cw = max((len(s["cmd"]) for s in steps), default=8) # align notes to the widest command
364
+ for i, stp in enumerate(steps):
365
+ lead = p("next".ljust(6), S.LABEL) if i == 0 else " " * 6
366
+ out.append(f" {lead} {p(stp['cmd'].ljust(cw), S.GOLD)} {p(stp['note'], S.CHROME)}")
367
+ if not steps: # runs stored before next_steps existed
368
+ out.append(f" {p('next'.ljust(6), S.LABEL)} "
369
+ f"{p('scaffold-test pins the wanted behavior; fix derives against it', S.GOLD)}")
370
+
350
371
  # ---- rules: standing preferences (intent made cumulative, recalled every build) ----
351
372
  if res.operation == "rules" and res.data:
352
373
  d = res.data
@@ -480,7 +501,7 @@ def render_human(res: SubstratumResult, stream=None, color: str | None = None) -
480
501
 
481
502
  out.append(p(S.g("foot") + S.g("bar") * 2, vcol))
482
503
  if not sm.get("full"): # the map is the summary; --full is the full reference
483
- out.append(f" {p('detail'.ljust(6), S.LABEL)} {p('substratum summarize --full', S.GOLD)}"
504
+ out.append(f" {p('next'.ljust(6), S.LABEL)} {p('summarize --full'.ljust(8), S.GOLD)}"
484
505
  + p(f" every one of {sm['functions']} functions {S.g('dot')} {sm['types']} types",
485
506
  S.CHROME))
486
507
  return "\n".join(out) + "\n"
@@ -38,7 +38,8 @@ _ALL_COMMANDS = set(_COMMANDS) | {"summary", "scaffold", "log", "hist", "tests",
38
38
  "login", "logout", "whoami", "?", "h",
39
39
  # session-state (the project-memory moat)
40
40
  "why", "blame-decision", "blame", "recheck",
41
- "remember", "forget", "rules", "sessions", "gc"}
41
+ "remember", "forget", "rules", "sessions", "gc",
42
+ "where", "locate", "open"}
42
43
 
43
44
  # commands whose argument is a run-id (for tab-completion + defaulting to the last run)
44
45
  _RUNID_CMDS = {"apply", "undo", "commit", "show", "explain", "replay", "attest"}
@@ -63,6 +64,7 @@ class Session:
63
64
  self.last_run: Optional[str] = None
64
65
  self.last_applied: Optional[str] = None
65
66
  self.last_op: Optional[str] = None
67
+ self._last_places: list = [] # files from the last `where`, for `open <n>`
66
68
  # PERSISTENT recall: the build's query trail carries across sessions (!N / !! / up-arrow see the
67
69
  # whole build, not just this process). Seeded from the ledger; appended to it on every query.
68
70
  from . import build_state
@@ -249,7 +251,7 @@ class Session:
249
251
  rail = ("\033[38;5;154m▌\033[0m" if (self.color and not ascii_mode) else "|")
250
252
  brand = (f"\033[38;5;154m{S.g('brand')}\033[0m" if (self.color and not ascii_mode)
251
253
  else pn(S.g("brand"), S.PROVEN))
252
- self._say(brand + " " + pn("SUBSTRATUM", S.BOLD) + pn(" proof-carrying · deterministic", S.LABEL))
254
+ self._say(brand + " " + pn("SUBSTRATUM", S.BOLD) + pn(" Proof-Carrying · Deterministic", S.LABEL))
253
255
  self._say(f" {rail} " + pn(name, S.BOLD) + " " + pn(census, S.LABEL))
254
256
  self._say(f" {rail} " + pn(substrate, S.LABEL) + " " + state)
255
257
  if st:
@@ -314,6 +316,9 @@ class Session:
314
316
  # balanced quotes and never take this path.
315
317
  parts = line.split()
316
318
  cmd, args = parts[0], parts[1:]
319
+ # the pasted shell form ('substratum summarize --full') works verbatim in-session, flags kept
320
+ if cmd.lower() == "substratum" and args:
321
+ cmd, args = args[0], args[1:]
317
322
  slashed = bool(cmd) and cmd[0] in "/:\\" # /exit, :q -- an explicit control command
318
323
  if slashed:
319
324
  cmd = cmd[1:]
@@ -364,6 +369,19 @@ class Session:
364
369
  else:
365
370
  self._say(" usage: why <file-or-symbol> (e.g. why auth.py)", _CYAN)
366
371
  return None, True
372
+ # `open` takes a result number or a path -- a sentence ('open a pull request') is not a
373
+ # target; re-route it so prose beginning with 'open' never hits the editor door. A bare
374
+ # token that is a real file (Makefile, LICENSE) counts as a target even without a dot.
375
+ elif cmd == "open" and (not args or not (
376
+ args[0].isdigit()
377
+ or any(c in args[0] for c in "./_:\\")
378
+ or os.path.exists(os.path.join(self.repo, args[0].split(":", 1)[0])))):
379
+ rerouted = self._route_nl(line)
380
+ if rerouted is not None:
381
+ cmd, args = rerouted
382
+ else:
383
+ self._say(" usage: open <n> (a number from the last where) or open <file[:line]>", _CYAN)
384
+ return None, True
367
385
 
368
386
  if cmd in ("help", "?", "h"):
369
387
  self._help(" ".join(args))
@@ -463,6 +481,39 @@ class Session:
463
481
  if cmd == "rules":
464
482
  from .commands.rules import cmd_rules
465
483
  return cmd_rules(self.repo), True
484
+ if cmd in ("where", "locate"):
485
+ # 'where did we leave off' is the catch-up idiom, not a grounding query
486
+ if cmd == "where" and re.search(r"^(did\s+|do\s+)?we\s+(left|leave)\s+off", " ".join(args).lower()):
487
+ from .commands.history import cmd_runs
488
+ return cmd_runs(self.repo), True
489
+ if not args:
490
+ self._say(' usage: where <what you are looking for> (e.g. where rate limiter)', _CYAN)
491
+ return None, True
492
+ from .commands.where import cmd_where
493
+ res = cmd_where(self.repo, " ".join(args))
494
+ if res is not None:
495
+ self._last_places = list((res.data or {}).get("files") or []) # `open <n>` targets
496
+ return res, True
497
+ if cmd == "open":
498
+ target = args[0]
499
+ if target.isdigit():
500
+ n = int(target)
501
+ if not self._last_places:
502
+ self._say(" nothing to open yet -- run where <words> first", _CYAN)
503
+ return None, True
504
+ if not 1 <= n <= len(self._last_places):
505
+ self._say(f" open takes 1..{len(self._last_places)} (from the last where)", _CYAN)
506
+ return None, True
507
+ place = self._last_places[n - 1]
508
+ hits = place.get("hits") or []
509
+ self._open_file(place["path"], hits[0]["line"] if hits else None)
510
+ return None, True
511
+ path, _, ln = target.rpartition(":")
512
+ if path and ln.isdigit():
513
+ self._open_file(path, int(ln))
514
+ else:
515
+ self._open_file(target, None)
516
+ return None, True
466
517
  if cmd == "sessions":
467
518
  from .commands.history import cmd_sessions
468
519
  return cmd_sessions(self.repo), True
@@ -582,6 +633,10 @@ class Session:
582
633
  if re.search(r"\b(summar\w+|tl;?dr)\b", low) or \
583
634
  re.search(r"\b(describe|explain|overview|walk me through|what(?:'s| is| does| do| are)?)\b"
584
635
  r".{0,40}\b(code|codebase|repo|repository|project|module|files?)\b", low):
636
+ # 'the FULL summary' / 'every function' carries through to --full, never silently dropped
637
+ if re.search(r"\bfull\b|\bevery\w*\b|\ball of it\b|\bin depth\b|\bdetailed?\b", low):
638
+ interp("summarize the codebase (full: every symbol)")
639
+ return ("summarize", ["--full"])
585
640
  interp("summarize the codebase")
586
641
  return ("summarize", [])
587
642
  # verify current changes -- 'verify' is unambiguous; gate/check/review/validate need a change object
@@ -657,6 +712,15 @@ class Session:
657
712
  if m:
658
713
  interp(f"why {m.group(1)} (the decisions that shaped it)")
659
714
  return ("why", [m.group(1)])
715
+ # a CHANGE REQUEST in domain language ('can we remove the spending check option from the
716
+ # workflows section?') -> ground the named terms in THIS repo + show the verified-change
717
+ # ladder. Never 'not a command' for legitimate engineering intent.
718
+ if re.search(r"\b(remove|delete|disable|enable|add|rename|move|change|modify|update|"
719
+ r"refactor|implement|hide|show)\b", low):
720
+ from .commands.where import extract_terms
721
+ if len(extract_terms(low)) >= 1:
722
+ interp("ground it in this repo (where does that live?)")
723
+ return ("where", line.split())
660
724
  # a stated standing rule ('never use print statements here') -> suggest recording it, never
661
725
  # auto-record (setting a rule is deliberate; see remember/rules)
662
726
  if re.search(r"^(never|always|do not|don'?t)\b.*\b(use|using|write|import|call|commit|push|"
@@ -791,6 +855,40 @@ class Session:
791
855
  return None # skipped: the ASK was already shown
792
856
  return res
793
857
 
858
+ def _open_file(self, path: str, line_no=None) -> None:
859
+ """Open a repo file in the user's editor, at the line when the editor can. Closed-class:
860
+ $VISUAL/$EDITOR first, then well-known GUI editors on PATH; the fallback is the path itself
861
+ (never an error -- the user came here to find a place, and the place is the answer)."""
862
+ import shutil
863
+ import subprocess
864
+ loc_note = f"{path}:{line_no}" if line_no else path
865
+ abspath = os.path.join(self.repo, path)
866
+ if not os.path.exists(abspath):
867
+ self._say(f" {path} is not in this repo (was it moved since that where?)", _CYAN)
868
+ return
869
+ raw = os.environ.get("VISUAL") or os.environ.get("EDITOR") or ""
870
+ editor = shlex.split(raw) if raw else []
871
+ if not editor:
872
+ found = next((c for c in ("code", "cursor", "code-insiders", "subl", "zed")
873
+ if shutil.which(c)), None)
874
+ editor = [found] if found else []
875
+ if not editor:
876
+ self._say(f" no editor found ($EDITOR is unset) -- it lives at {loc_note}", _CYAN)
877
+ return
878
+ base = os.path.basename(editor[0])
879
+ gui = {"code": "-g", "code-insiders": "-g", "cursor": "-g", "subl": "", "zed": ""}
880
+ try:
881
+ if base in gui: # GUI editors: path:line syntax, detached
882
+ loc = f"{abspath}:{line_no}" if line_no else abspath
883
+ cmd = editor + ([gui[base], loc] if gui[base] else [loc])
884
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
885
+ self._say(f" opened {loc_note} in {base}", _DIM)
886
+ else: # terminal editors: +line, and hand over the tty
887
+ cmd = editor + ([f"+{line_no}"] if line_no else []) + [abspath]
888
+ subprocess.run(cmd, check=False)
889
+ except OSError as e:
890
+ self._say(f" could not start {base} ({e}) -- it lives at {loc_note}", _CYAN)
891
+
794
892
  def _verify(self, args):
795
893
  from .commands.verify import run_verify
796
894
  # flags: --staged, --test <t> (repeatable). default = working-tree diff.
@@ -837,6 +935,8 @@ class Session:
837
935
  ("UNDERSTAND", "", [
838
936
  ("summarize", "describe this codebase (add --full for every symbol)"),
839
937
  ("failing", "run the tests and list what is failing"),
938
+ ("where <words>", "find where something lives in this repo (grounds your words in code)"),
939
+ ("open <n|file[:line]>", "open a found place in your editor (n from the last where)"),
840
940
  ]),
841
941
  ("BUILD", "verified or refused, never a guess", [
842
942
  ("fix <test>", "a verified fix -- a failing test, a css file, or a css intent (help fix)"),
@@ -863,6 +963,8 @@ class Session:
863
963
  ("doctor", "check the environment, harness, and connection"),
864
964
  ("init", "map what this repo can gate (the coverage contract)"),
865
965
  ("login", "connect the hosted engine for fix and write"),
966
+ ("logout", "disconnect this machine (local commands keep working)"),
967
+ ("whoami", "who is logged in, and to which server"),
866
968
  ]),
867
969
  ]
868
970
  _HELP_ALSO = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: substratum-cli
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Substratum: verified-or-refused code, with provenance. The terminal over the comprehension engine.
5
5
  Author: Substratum
6
6
  License: LicenseRef-Proprietary
@@ -56,6 +56,7 @@ substratum_cli/commands/scaffold.py
56
56
  substratum_cli/commands/show.py
57
57
  substratum_cli/commands/undo.py
58
58
  substratum_cli/commands/verify.py
59
+ substratum_cli/commands/where.py
59
60
  substratum_cli/commands/write.py
60
61
  tests/test_adaptive_prior.py
61
62
  tests/test_alias_warrant_trace.py
File without changes
File without changes