substratum-cli 0.1.0__tar.gz → 0.2.1__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 (111) hide show
  1. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/PKG-INFO +1 -1
  2. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/pyproject.toml +4 -1
  3. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/__init__.py +3 -1
  4. substratum_cli-0.2.1/substratum_cli/_boot.py +166 -0
  5. substratum_cli-0.2.1/substratum_cli/_update.py +58 -0
  6. substratum_cli-0.2.1/substratum_cli/_wordmark.py +12 -0
  7. substratum_cli-0.2.1/substratum_cli/build_state.py +118 -0
  8. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/cli.py +11 -1
  9. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/codes.py +3 -1
  10. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/apply.py +2 -0
  11. substratum_cli-0.2.1/substratum_cli/commands/failing.py +149 -0
  12. substratum_cli-0.2.1/substratum_cli/commands/pr.py +80 -0
  13. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/gitio.py +16 -0
  14. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/product_ops.py +70 -9
  15. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/refusals.py +10 -2
  16. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/render.py +75 -27
  17. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/session.py +186 -68
  18. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/style.py +31 -6
  19. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/PKG-INFO +1 -1
  20. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/SOURCES.txt +5 -0
  21. substratum_cli-0.1.0/substratum_cli/commands/failing.py +0 -80
  22. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/README.md +0 -0
  23. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/setup.cfg +0 -0
  24. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/README.md +0 -0
  25. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/__main__.py +0 -0
  26. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/_vendor/__init__.py +0 -0
  27. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/_vendor/_pro_test_derive.py +0 -0
  28. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/account.py +0 -0
  29. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/__init__.py +0 -0
  30. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/auth.py +0 -0
  31. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/commit.py +0 -0
  32. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/explain.py +0 -0
  33. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/fix.py +0 -0
  34. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/history.py +0 -0
  35. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/init_cmd.py +0 -0
  36. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/replay.py +0 -0
  37. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/scaffold.py +0 -0
  38. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/show.py +0 -0
  39. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/undo.py +0 -0
  40. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/verify.py +0 -0
  41. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/commands/write.py +0 -0
  42. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/config.py +0 -0
  43. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/diffs.py +0 -0
  44. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/engine.py +0 -0
  45. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/langscan.py +0 -0
  46. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/remote.py +0 -0
  47. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/result.py +0 -0
  48. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/run_store.py +0 -0
  49. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli/runid.py +0 -0
  50. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/dependency_links.txt +0 -0
  51. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/entry_points.txt +0 -0
  52. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/requires.txt +0 -0
  53. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/substratum_cli.egg-info/top_level.txt +0 -0
  54. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_adaptive_prior.py +0 -0
  55. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_alias_warrant_trace.py +0 -0
  56. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_bidirectional_gate_contracts.py +0 -0
  57. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_coherence_evolution.py +0 -0
  58. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_coherences.py +0 -0
  59. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_connection_web_api.py +0 -0
  60. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_cross_entity_scoping.py +0 -0
  61. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_data.py +0 -0
  62. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_de28_negation_refuse.py +0 -0
  63. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_definite_description_handler.py +0 -0
  64. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_demo_regression_catalog.py +0 -0
  65. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_demo_response_shape.py +0 -0
  66. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_e1_e2_field_alignment.py +0 -0
  67. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_extraction_class_write_time.py +0 -0
  68. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_falsification_predictions.py +0 -0
  69. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_gap_d_wh_normalization.py +0 -0
  70. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_inference.py +0 -0
  71. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_inflection_cvc_rule.py +0 -0
  72. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_inflection_irregular.py +0 -0
  73. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_intervention.py +0 -0
  74. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_invariant.py +0 -0
  75. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_legal_citation_handler.py +0 -0
  76. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_lens_noop_seam.py +0 -0
  77. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_markup_tag_handler.py +0 -0
  78. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_nl_parser.py +0 -0
  79. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_nl_parser_adversarial.py +0 -0
  80. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_noise.py +0 -0
  81. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_pass2_contextual.py +0 -0
  82. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_perspectives.py +0 -0
  83. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_polysemous_token_alias_guard.py +0 -0
  84. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_possessive_normalization.py +0 -0
  85. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_predicate_canon.py +0 -0
  86. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_projection_gate.py +0 -0
  87. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_projection_gate_tag_question.py +0 -0
  88. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_query_normalizer.py +0 -0
  89. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_records_answer_format.py +0 -0
  90. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_referent_aboutness_gate.py +0 -0
  91. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_referent_quality_gate.py +0 -0
  92. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_refuse_message_topic_neutrality.py +0 -0
  93. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_relation_detection.py +0 -0
  94. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_relaxation.py +0 -0
  95. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_residual_content_gate.py +0 -0
  96. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_reverse_lookup_subject_filter.py +0 -0
  97. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_sec_statement_aliases.py +0 -0
  98. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_span_lens.py +0 -0
  99. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_span_lens_query_integration.py +0 -0
  100. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_substrate.py +0 -0
  101. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_synonymy_detector.py +0 -0
  102. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_table_ingest.py +0 -0
  103. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_triple_ingest.py +0 -0
  104. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_two_phase_schema.py +0 -0
  105. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_user_asserted_aliases.py +0 -0
  106. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_v2_api.py +0 -0
  107. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_v2_api_claims.py +0 -0
  108. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_v2_claim_determinism.py +0 -0
  109. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_word_role.py +0 -0
  110. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/tests/test_word_role_wiring.py +0 -0
  111. {substratum_cli-0.1.0 → substratum_cli-0.2.1}/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.1.0
3
+ Version: 0.2.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "substratum-cli"
7
- version = "0.1.0"
7
+ dynamic = ["version"] # read from substratum_cli.__version__ (the single source)
8
8
  description = "Substratum: verified-or-refused code, with provenance. The terminal over the comprehension engine."
9
9
  readme = "substratum_cli/README.md"
10
10
  license = { text = "LicenseRef-Proprietary" }
@@ -35,5 +35,8 @@ Homepage = "https://substratum-api.fly.dev"
35
35
  [tool.setuptools]
36
36
  packages = ["substratum_cli", "substratum_cli.commands", "substratum_cli._vendor"]
37
37
 
38
+ [tool.setuptools.dynamic]
39
+ version = { attr = "substratum_cli.__version__" }
40
+
38
41
  [tool.pytest.ini_options]
39
42
  markers = ["slow: loads the full production library (minutes); excluded from the fast gate"]
@@ -3,4 +3,6 @@
3
3
  Import budget: this package is stdlib-only; the ONLY module reaching into backend.* is
4
4
  `engine.py`, lazily. Importing substratum_cli (or any light command) opens nothing heavy.
5
5
  """
6
- __version__ = "0.1.0"
6
+ # SINGLE SOURCE OF TRUTH for the release version. pyproject reads this (dynamic version), the boot shows
7
+ # it, and the update-check compares against it. To cut a release, bump ONLY this line, then publish.
8
+ __version__ = "0.2.1"
@@ -0,0 +1,166 @@
1
+ """The Substratum boot identity: THE PROOF SEAL.
2
+
3
+ Not a wordmark splash -- a self-certifying certificate. The seal (a hash of engine build + substrate +
4
+ scheme, with ZERO time/session input) prints byte-identical on every boot, because the engine that
5
+ draws it is deterministic. A token-streaming tool cannot render a stable self-certificate; the sameness
6
+ IS the demonstration. Below the emblem (the logo: a flat plane struck by two rays converging on one lime
7
+ node) sits a structured, color-hierarchied ledger. Stdlib only; pure given its inputs.
8
+
9
+ Color law: lime(154)=brand/identity only, 190=the ONE bright spark (the convergence node), green(42)=
10
+ proven, gold(179)=the single next move, grey(245 label / 240 chrome / 252 data)=structure + values.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+
16
+ # The PUBLIC substrate release name -- what the product shows, like a model version (GPT-5.2, Opus 4.8).
17
+ # Decoupled from the internal mining tag (pattern_library_scaled_v15): that tag still drives the seal
18
+ # hash (the seal must certify the ACTUAL build), but users see this. Bump only on a real public release.
19
+ RELEASE = "Substrate 1.0 Preview"
20
+
21
+ L = "\033[38;5;154m" # brand lime
22
+ LB = "\033[1;38;5;190m" # the one bright spark
23
+ GR = "\033[38;5;42m" # proven green
24
+ GO = "\033[38;5;179m" # the next move
25
+ LAB = "\033[38;5;245m" # labels
26
+ CH = "\033[38;5;240m" # chrome rails
27
+ DAT = "\033[38;5;252m" # data values
28
+ B = "\033[1m"
29
+ R = "\033[0m"
30
+
31
+ # the emblem: THE STRUCK-BY-LIGHT SLAB (founder pick). A flat substrate plane seen edge-on where the
32
+ # light is literal -- a brightness gradient (█▓▒░) converging from the solid lit edges to one bright ◇
33
+ # node at the core. Built ONLY from cell-filling block glyphs so it renders as one connected shape;
34
+ # diagonal box-drawing (╱ ╲) was tried and FRAGMENTS in real terminal fonts (it does not tile).
35
+ _EMBLEM = [
36
+ "▄███████████████████▄",
37
+ "███▓▓▓▒▒░░◇░░▒▒▓▓▓███",
38
+ "▀███████████████████▀",
39
+ ]
40
+ _EMBLEM_ASCII = [
41
+ "#####################",
42
+ "###+++==--*--==+++###",
43
+ "#####################",
44
+ ]
45
+
46
+
47
+ def seal(lib_name: str, version: str) -> tuple[str, str]:
48
+ """(sigil, digest) -- a deterministic self-certificate. No time/session input, so it is byte-identical
49
+ every boot. The digest is a stable proof that this engine+substrate render the same thing every run."""
50
+ h = hashlib.sha256(f"substratum|scheme:sub1|{version}|{lib_name}".encode()).hexdigest()
51
+ sigil = "".join(chr(0x2800 + int(h[i:i + 2], 16)) for i in range(0, 8, 2)) # 4 braille glyphs
52
+ digest = f"{h[0:4]} {h[4:8]} {h[8:12]} {h[12:16]}"
53
+ return sigil, digest
54
+
55
+
56
+ def render(name, census, langs, libname, patterns, warm, standing, version,
57
+ color=True, ascii_mode=False, logged_in=True, update_to="") -> list[str]:
58
+ """Return the boot lines. `standing` is build_state.standing() (may be {})."""
59
+ def c(code, s):
60
+ return f"{code}{s}{R}" if color else s
61
+
62
+ sig, dig = seal(libname, version)
63
+ if ascii_mode:
64
+ sig = "#" + dig.split()[0]
65
+ dot = "-" if ascii_mode else "·" # separators degrade with the glyph set
66
+ if ascii_mode: # the renderer guarantees ascii output regardless of input
67
+ name, census, langs, libname = (str(s).replace("·", "-") for s in (name, census, langs, libname))
68
+ W = 78 # certificate inner width
69
+ out = ["", ""]
70
+
71
+ # ---- EMBLEM (the struck plane) ----
72
+ emb = _EMBLEM_ASCII if ascii_mode else _EMBLEM
73
+ spark = "*" if ascii_mode else "◇"
74
+ for ln in emb:
75
+ if color:
76
+ body = (L + ln.replace(spark, R + LB + spark + R + L) + R) if spark in ln else (L + ln + R)
77
+ else:
78
+ body = ln
79
+ out.append(" " + body)
80
+
81
+ # ---- WORDMARK + tagline ----
82
+ wm = " ".join("SUBSTRATUM")
83
+ tag = "proof-carrying code engine" + c(CH, " · ") + "zero-LLM" + c(CH, " · ") + "deterministic"
84
+ out += ["", " " + c(L + B, wm) + " " + c(LAB, "proof-carrying code engine")
85
+ + c(CH, f" {dot} ") + c(LAB, "zero-LLM") + c(CH, f" {dot} ") + c(LAB, "deterministic"), ""]
86
+
87
+ # ---- the CERTIFICATE ----
88
+ tl, tr, bl, br, h_, v_, ml, mr = ("+", "+", "+", "+", "-", "|", "+", "+") if ascii_mode \
89
+ else ("┌", "┐", "└", "┘", "─", "│", "├", "┤")
90
+
91
+ def bar(top=False, bottom=False, label=""):
92
+ if label: # section divider: measure the PLAIN label width
93
+ dashes = h_ * max(0, W - len(label) - 3)
94
+ return c(CH, ml + h_ + " ") + c(LAB + B, label) + c(CH, " " + dashes + mr)
95
+ lc, rc = (tl, tr) if top else (bl, br)
96
+ return c(CH, lc + h_ * W + rc)
97
+
98
+ def rowline(inner_plain, inner_colored):
99
+ pad = " " * max(0, W - 1 - _vis(inner_plain))
100
+ return c(CH, v_) + " " + inner_colored + pad + c(CH, v_)
101
+
102
+ lamp = (("*" if ascii_mode else "●") + " ready") if warm else (dot + " loading")
103
+ lampc = c(GR, lamp) if warm else c(CH, lamp)
104
+ st = standing or {}
105
+ runs, ver = st.get("runs", 0), st.get("verified", 0)
106
+ unsaved = st.get("unapplied_total", 0)
107
+
108
+ box = [bar(top=True)]
109
+ seal_inner_p = f"SEAL {sig} sub1{dot}{dig}"
110
+ seal_inner_c = c(LAB, "SEAL") + " " + c(L, sig) + " " + c(L, f"sub1{dot}") + c(LAB, dig)
111
+ right_p = f"deterministic {dot} replayable"
112
+ right_c = c(GR, "deterministic") + c(CH, f" {dot} ") + c(GR, "replayable")
113
+ pad = " " * max(1, W - 1 - _vis(seal_inner_p) - _vis(right_p))
114
+ box.append(c(CH, v_) + " " + seal_inner_c + pad + right_c + c(CH, v_))
115
+
116
+ box.append(bar(label="SUBSTRATE"))
117
+ box.append(rowline(f" release {RELEASE} {lamp}",
118
+ c(LAB, " release ") + c(DAT, RELEASE) + " " + lampc))
119
+ box.append(rowline(f" covers {langs}", c(LAB, " covers ") + c(DAT, langs)))
120
+ box.append(bar(label="REPO"))
121
+ box.append(rowline(f" project {name}", c(LAB, " project ") + c(DAT, name)))
122
+ box.append(rowline(f" files {census}", c(LAB, " files ") + c(DAT, census)))
123
+ box.append(bar(label="SESSION"))
124
+ ok_p = "✓" if not ascii_mode else "[ok]"
125
+ sess_p = f" runs {runs} verified {ver} {ok_p} refused 0 unsaved {unsaved}"
126
+ ok = c(GR, "✓") if not ascii_mode else "[ok]"
127
+ sess_c = (c(LAB, " runs ") + c(DAT, str(runs)) + c(LAB, " verified ") + c(GR, str(ver)) + " " + ok
128
+ + c(LAB, " refused ") + c(DAT, "0")
129
+ + c(LAB, " unsaved ") + (c(GO, str(unsaved)) if unsaved else c(DAT, "0")))
130
+ box.append(rowline(sess_p, sess_c))
131
+ box.append(bar(bottom=True))
132
+ out += [" " + b for b in box]
133
+
134
+ # ---- the PROVABLE RESUME (only when there is verified, unapplied work) ----
135
+ up = (st.get("unapplied") or [])
136
+ if up:
137
+ r0 = up[0]
138
+ what = f"{r0['operation']} {r0.get('target', '')}".strip()
139
+ out += ["", " " + c(GO, ">" if ascii_mode else "▸") + c(LAB, " welcome back")
140
+ + c(CH, " -- ") + c(LAB, f"{unsaved} verified change from a prior session is unsaved"),
141
+ " " + c(GO, "apply " + r0["run_id"]) + c(CH, " writes ") + c(DAT, what)
142
+ + c(CH, f" into your files {dot} ") + c(GR, "byte-identical")]
143
+
144
+ # ---- the SIGNPOST: never leave a user guessing what they can do, or unaware they must connect.
145
+ # fix + write run on the hosted engine; summarize/failing/verify run offline. Say so BEFORE they hit
146
+ # a NO_ENGINE refusal, and pick an ask-example that actually works in their current state.
147
+ ask_ex = '"fix the failing test"' if logged_in else '"what does this codebase do?"'
148
+ # the signpost text aligns at col 4 -- the same column as the welcome-back text -- so the whole bottom
149
+ # "here is what you can do" region reads as one clean left edge instead of jutting out under the box.
150
+ out += [""]
151
+ if update_to: # a pip package cannot self-update; hand over the one command
152
+ arrow = "^" if ascii_mode else "↑"
153
+ out += [" " + c(GO, f"{arrow} update") + c(LAB, f" Substratum {update_to} is available")
154
+ + c(CH, f" {dot} ") + c(GO, "pip install --upgrade substratum-cli")]
155
+ if not logged_in:
156
+ out += [" " + c(GO, "login") + c(LAB, " to ") + c(DAT, "fix") + c(LAB, " and ") + c(DAT, "write")
157
+ + c(LAB, " code (connects the engine)")
158
+ + c(CH, f" {dot} ") + c(LAB, "summarize, failing, verify work offline")]
159
+ out += [" " + c(GO, "help") + c(LAB, " see everything you can do")
160
+ + c(CH, f" {dot} ") + c(LAB, "or just ask: ") + c(GO, ask_ex)]
161
+ return out
162
+
163
+
164
+ def _vis(s: str) -> int:
165
+ import re
166
+ return len(re.sub(r"\033\[[0-9;]*m", "", s))
@@ -0,0 +1,58 @@
1
+ """Best-effort update signpost. A pip package cannot push itself to an installed user, so the most we can
2
+ do is tell them, on boot, that a newer version exists and hand them the one command that gets it. That way
3
+ every iteration we publish is picked up by the user's next session -- they just run the upgrade.
4
+
5
+ Design guarantees: NEVER blocks boot, NEVER raises, NEVER sends user code anywhere (it reads one version
6
+ string from the single host we already talk to). The read is instant (from a tiny cache under
7
+ ~/.substratum/); a background thread refreshes that cache for the next session. So the nag is at most one
8
+ session behind a publish -- the industry-standard pattern (npm/gh do the same)."""
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import threading
14
+ import time
15
+
16
+
17
+ def _cache_path() -> str:
18
+ return os.path.join(os.path.expanduser("~"), ".substratum", "update.json")
19
+
20
+
21
+ def _version_tuple(s: str) -> tuple:
22
+ """Compare X.Y.Z numerically; non-numeric suffixes (a 'Preview' etc.) reduce to their digits or 0."""
23
+ out = []
24
+ for part in str(s or "").split("."):
25
+ digits = "".join(ch for ch in part if ch.isdigit())
26
+ out.append(int(digits) if digits else 0)
27
+ return tuple(out)
28
+
29
+
30
+ def available(current: str) -> str:
31
+ """The newer published version if the cache knows of one, else "". Pure, instant, no network."""
32
+ try:
33
+ latest = str(json.load(open(_cache_path(), encoding="utf-8")).get("latest") or "")
34
+ except Exception:
35
+ return ""
36
+ return latest if latest and _version_tuple(latest) > _version_tuple(current) else ""
37
+
38
+
39
+ def refresh_async() -> None:
40
+ """Fire-and-forget: ask the server for the latest published client and cache it for next session.
41
+ The server sources this from PyPI, so publishing is all it takes -- no redeploy, no constant to bump."""
42
+ def _w():
43
+ try:
44
+ from . import remote
45
+ info = remote.server_info() or {}
46
+ latest = str(info.get("latest_client") or "")
47
+ if not latest:
48
+ return
49
+ path = _cache_path()
50
+ os.makedirs(os.path.dirname(path), exist_ok=True)
51
+ with open(path, "w", encoding="utf-8") as fh:
52
+ json.dump({"latest": latest, "t": int(time.time())}, fh)
53
+ except Exception:
54
+ pass
55
+ try:
56
+ threading.Thread(target=_w, daemon=True).start()
57
+ except Exception:
58
+ pass
@@ -0,0 +1,12 @@
1
+ """The SUBSTRATUM wordmark (figlet ansi_shadow), generated OFFLINE and stored as a plain
2
+ string so the CLI stays zero-dependency. The brand mark: bold blocks, brand-lime (#C9FF3E)."""
3
+ WORDMARK = [
4
+ '███████╗██╗ ██╗██████╗ ███████╗████████╗██████╗ █████╗ ████████╗██╗ ██╗███╗ ███╗',
5
+ '██╔════╝██║ ██║██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██║ ██║████╗ ████║',
6
+ '███████╗██║ ██║██████╔╝███████╗ ██║ ██████╔╝███████║ ██║ ██║ ██║██╔████╔██║',
7
+ '╚════██║██║ ██║██╔══██╗╚════██║ ██║ ██╔══██╗██╔══██║ ██║ ██║ ██║██║╚██╔╝██║',
8
+ '███████║╚██████╔╝██████╔╝███████║ ██║ ██║ ██║██║ ██║ ██║ ╚██████╔╝██║ ╚═╝ ██║',
9
+ '╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝',
10
+ ' ',
11
+ ]
12
+ WIDTH = 87
@@ -0,0 +1,118 @@
1
+ """The build ledger: `.substratum/ledger.jsonl`. A build is a long working-through, not isolated
2
+ commands -- so every query, decision, and apply is recorded, perfectly, for the life of the build.
3
+ This is NOT a context window (nothing is summarized or forgotten); it is durable, addressable state
4
+ that persists across sessions and grows into grounding. The run store holds the PROOFS; this holds
5
+ the DECISION TRAIL over them and the standing state a session reads on open. Stdlib only, append-only,
6
+ atomic. Complements run_store; never touches the engine (zero-wrong unaffected)."""
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ from typing import Optional
13
+
14
+ from . import run_store
15
+
16
+
17
+ def _path(repo: str) -> str:
18
+ return os.path.join(run_store.store_dir(repo), "ledger.jsonl")
19
+
20
+
21
+ def record(repo: str, kind: str, ts: Optional[int] = None, **fields) -> None:
22
+ """Append one event. kind: 'query' | 'decision' | 'apply' | 'commit'. Never raises."""
23
+ try:
24
+ rec = {"t": int(ts if ts is not None else time.time()), "kind": kind, **fields}
25
+ os.makedirs(os.path.dirname(_path(repo)), exist_ok=True)
26
+ with open(_path(repo), "a", encoding="utf-8") as fh:
27
+ fh.write(json.dumps(rec) + "\n")
28
+ except Exception:
29
+ pass
30
+
31
+
32
+ def _events(repo: str) -> list:
33
+ p = _path(repo)
34
+ if not os.path.exists(p):
35
+ return []
36
+ out = []
37
+ for line in open(p, encoding="utf-8", errors="replace"):
38
+ line = line.strip()
39
+ if not line:
40
+ continue
41
+ try:
42
+ out.append(json.loads(line))
43
+ except Exception:
44
+ continue
45
+ return out
46
+
47
+
48
+ def queries(repo: str, limit: int = 200) -> list:
49
+ """The persistent query/command history (oldest->newest) so recall (!!, !N, up-arrow) survives
50
+ across sessions -- the keystroke trail of the whole build, not just this process."""
51
+ q = [e.get("text", "") for e in _events(repo) if e.get("kind") == "query" and e.get("text")]
52
+ return q[-limit:]
53
+
54
+
55
+ def applied_run_ids(repo: str) -> set:
56
+ return {e.get("run_id") for e in _events(repo) if e.get("kind") == "apply" and e.get("run_id")}
57
+
58
+
59
+ def decision_count(repo: str) -> int:
60
+ return sum(1 for e in _events(repo) if e.get("kind") == "decision")
61
+
62
+
63
+ def _norm_intent(s: str) -> str:
64
+ return " ".join((s or "").lower().split()) # prose: case-insensitive
65
+
66
+
67
+ def _norm_sig(s: str) -> str:
68
+ return " ".join((s or "").split()) # code: case-PRESERVED (Python is case-sensitive)
69
+
70
+
71
+ def record_decision(repo: str, intent: str, sig: str, purpose: str, into: str = None) -> None:
72
+ """Persist a disambiguation the user EXPLICITLY resolved to a VERIFIED output. Keyed on the full
73
+ disambiguating context (intent + signature + target file) so it is only recalled in the same
74
+ situation. Only ever recorded on an emit that carried an explicit pick."""
75
+ if not (intent and purpose):
76
+ return
77
+ record(repo, "decision", intent_n=_norm_intent(intent), sig_n=_norm_sig(sig),
78
+ into=(into or ""), purpose=str(purpose))
79
+
80
+
81
+ def recall_decision(repo: str, intent: str, sig: str, into: str = None) -> str:
82
+ """The purpose chosen LAST TIME for this EXACT (normalized) intent+signature+target, or "" if none.
83
+
84
+ SAFETY: this is used ONLY to PRESELECT a choice inside a clarification ASK -- it NEVER auto-forces
85
+ a reading and NEVER suppresses the ASK for a genuinely ambiguous intent (that would silently steer
86
+ the output away from the user's session intent and break cross-engineer determinism). It is a
87
+ surfaced hint the user re-confirms; the gate is untouched and the ASK is never deleted. The match
88
+ is a conservative EXACT (normalized, context-keyed) match; no fuzzy matching."""
89
+ inn, sn, it = _norm_intent(intent), _norm_sig(sig), (into or "")
90
+ got = ""
91
+ for e in _events(repo):
92
+ if (e.get("kind") == "decision" and e.get("intent_n") == inn and e.get("sig_n") == sn
93
+ and (e.get("into") or "") == it):
94
+ got = str(e.get("purpose") or "") # scan in order -> most-recent match wins
95
+ return got
96
+
97
+
98
+ def standing(repo: str) -> dict:
99
+ """The welcome-back summary a session shows on open, so the persistent state is FELT, not just
100
+ queryable: how much has been built, the last run, any verified work still waiting to be applied,
101
+ and how many decisions the build carries forward. Empty dict on a fresh repo."""
102
+ runs = run_store.list_runs(repo, limit=500)
103
+ if not runs and not os.path.exists(_path(repo)):
104
+ return {}
105
+ applied = applied_run_ids(repo)
106
+ unapplied = [r for r in runs
107
+ if r.get("verdict") in ("VERIFIED", "PASS")
108
+ and r.get("operation") in ("fix", "write")
109
+ and r.get("run_id") not in applied]
110
+ verified = sum(1 for r in runs if r.get("verdict") in ("VERIFIED", "PASS"))
111
+ return {
112
+ "runs": len(runs),
113
+ "verified": verified,
114
+ "last": runs[0] if runs else None,
115
+ "unapplied": unapplied[:3],
116
+ "unapplied_total": len(unapplied),
117
+ "decisions": decision_count(repo),
118
+ }
@@ -54,6 +54,7 @@ def build_parser() -> argparse.ArgumentParser:
54
54
  ("runs", "run history + the refusal->verified conversion ledger (the proof store)"),
55
55
  ("undo", "reverse-apply a run (the inverse of apply)"),
56
56
  ("commit", "proof-carrying git commit of a verified, applied run"),
57
+ ("pr", "open a proof-carrying pull request (pushes with your git; proof in the body)"),
57
58
  ("failing", "run the suite and list failing tests (each becomes a fix target)"),
58
59
  ]:
59
60
  p = sub.add_parser(name, help=help_)
@@ -66,6 +67,9 @@ def build_parser() -> argparse.ArgumentParser:
66
67
  p.add_argument("--failing-test", action="append", default=[])
67
68
  p.add_argument("--intent", default="")
68
69
  p.add_argument("--test-command", default=None)
70
+ if name == "summarize":
71
+ p.add_argument("--full", action="store_true",
72
+ help="list every symbol (lift the one-screen cap)")
69
73
  if name == "write":
70
74
  p.add_argument("intent", help="what the code should do, in plain language")
71
75
  p.add_argument("--sig", required=True, help="target signature, e.g. \"clamp(x, lo, hi)\"")
@@ -79,6 +83,9 @@ def build_parser() -> argparse.ArgumentParser:
79
83
  p.add_argument("--commit", default=None)
80
84
  p.add_argument("--test", action="append", default=[])
81
85
  p.add_argument("--test-command", default=None)
86
+ if name == "pr":
87
+ p.add_argument("run_id", nargs="?", default=None)
88
+ p.add_argument("--title", default=None)
82
89
  if name in ("apply", "show", "replay", "attest", "explain", "undo", "commit"):
83
90
  p.add_argument("run_id")
84
91
  if name == "commit":
@@ -126,7 +133,7 @@ def dispatch(args) -> "_result.SubstratumResult":
126
133
  return cmd_scaffold_test(args.repo, getattr(args, "target", None) or "")
127
134
  if cmd == "summarize":
128
135
  from . import product_ops
129
- return product_ops.op_summarize_repo(args.repo)
136
+ return product_ops.op_summarize_repo(args.repo, full=getattr(args, "full", False))
130
137
  if cmd == "runs":
131
138
  from .commands.history import cmd_runs
132
139
  return cmd_runs(args.repo)
@@ -136,6 +143,9 @@ def dispatch(args) -> "_result.SubstratumResult":
136
143
  if cmd == "commit":
137
144
  from .commands.commit import cmd_commit
138
145
  return cmd_commit(args.repo, args.run_id, getattr(args, "message", None))
146
+ if cmd == "pr":
147
+ from .commands.pr import cmd_pr
148
+ return cmd_pr(args.repo, getattr(args, "run_id", None), getattr(args, "title", None))
139
149
  if cmd == "failing":
140
150
  from .commands.failing import cmd_failing
141
151
  return cmd_failing(args.repo, path=getattr(args, "target", None),
@@ -38,12 +38,13 @@ FLAKY_OR_VACUOUS_WARRANT = "FLAKY_OR_VACUOUS_WARRANT" # 14: tests pass at base
38
38
  FLAKY_TEST = "FLAKY_TEST" # 14: quarantined nondeterministic test
39
39
  RUN_NOT_FOUND = "RUN_NOT_FOUND" # 11: show/explain/replay on an unknown id
40
40
  NO_ENGINE = "NO_ENGINE" # 12: synthesis needs the engine; login or [engine]
41
+ NO_TEST_RUNNER = "NO_TEST_RUNNER" # 12: `failing` found no test runner to launch here
41
42
 
42
43
  REFUSAL_CODES = (
43
44
  NO_GROUNDED_CANDIDATE, GATE_PRECONDITION, NO_WARRANT_TESTS, HUNK_CONTEXT_STALE,
44
45
  APPLY_STATE_DRIFT, REPLAY_STATE_DRIFT, NO_DRIVER, AUTONOMY_LOCKED,
45
46
  UNDERDETERMINED_INTENT, FLAKY_OR_VACUOUS_WARRANT, FLAKY_TEST, RUN_NOT_FOUND,
46
- NO_ENGINE,
47
+ NO_ENGINE, NO_TEST_RUNNER,
47
48
  )
48
49
 
49
50
  # ---- the exit-code table (closed; the machine contract) ----------------------
@@ -63,6 +64,7 @@ _REFUSAL_EXIT = {
63
64
  NO_DRIVER: 12,
64
65
  AUTONOMY_LOCKED: 12,
65
66
  NO_ENGINE: 12,
67
+ NO_TEST_RUNNER: 12,
66
68
  UNDERDETERMINED_INTENT: 13,
67
69
  FLAKY_OR_VACUOUS_WARRANT: 14,
68
70
  FLAKY_TEST: 14,
@@ -45,5 +45,7 @@ def cmd_apply(repo: str, run_id: str) -> "_result.SubstratumResult":
45
45
  ctx={"target": run_id, "run_id": run_id}))
46
46
  finally:
47
47
  os.unlink(patch)
48
+ from .. import build_state
49
+ build_state.record(repo, "apply", run_id=run_id, files=list(res.files)) # standing-state feed
48
50
  return _result.make(codes.PASS, run_id, "apply", files=res.files,
49
51
  data={"applied": list(res.files)})
@@ -0,0 +1,149 @@
1
+ """`failing` / `tests` -- discover failing tests in the repo by RUNNING its own suite and collecting
2
+ the failed node-ids. This is an OBSERVATION (read-only; runs the user's existing test command), not a
3
+ guess -- the failing tests are facts from the runner, and each becomes a ready `fix <id>` target.
4
+
5
+ Detects the harness per language: pytest (Python), go test (Go), vitest / jest / the project's own
6
+ `test` script (JS/TS). Failed ids are parsed reliably for pytest + go; for JS runners we report the
7
+ failed COUNT from the summary (never invent node-ids -- zero-wrong)."""
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import shlex
14
+ import subprocess
15
+ import sys as _sys
16
+ from typing import Optional
17
+
18
+ from .. import codes, refusals, runid
19
+ from .. import result as _result
20
+
21
+ # reliable, format-stable id parsers
22
+ _ID_PARSERS = {
23
+ "pytest": re.compile(r"^(?:FAILED|ERROR)\s+(\S+)", re.M), # FAILED path::test - reason
24
+ "go": re.compile(r"^\s*--- FAIL:\s+(\S+)", re.M), # --- FAIL: TestName (0.00s)
25
+ }
26
+ # JS runners: parse the failed COUNT from the summary (names vary by reporter; do not fabricate ids)
27
+ _COUNT_PARSERS = {
28
+ "vitest": re.compile(r"Tests\s+(\d+)\s+failed", re.I),
29
+ "jest": re.compile(r"Tests:\s+(\d+)\s+failed", re.I),
30
+ "node": re.compile(r"(\d+)\s+failed", re.I),
31
+ }
32
+
33
+
34
+ def _read(p: str) -> str:
35
+ try:
36
+ return open(p, encoding="utf-8", errors="replace").read()
37
+ except Exception:
38
+ return ""
39
+
40
+
41
+ def _pkg(repo: str):
42
+ p = os.path.join(repo, "package.json")
43
+ if not os.path.isfile(p):
44
+ return None
45
+ try:
46
+ return json.loads(_read(p))
47
+ except Exception:
48
+ return {}
49
+
50
+
51
+ def _node_pm(repo: str) -> str:
52
+ if os.path.exists(os.path.join(repo, "pnpm-lock.yaml")):
53
+ return "pnpm"
54
+ if os.path.exists(os.path.join(repo, "yarn.lock")):
55
+ return "yarn"
56
+ return "npm"
57
+
58
+
59
+ def _detect_command(repo: str):
60
+ """Return (argv, kind) where kind selects the failure parser, or None if no runner is found."""
61
+ j = os.path.join
62
+ # ---- Python: pytest ----
63
+ for marker in ("pytest.ini", "conftest.py", "tox.ini"):
64
+ if os.path.exists(j(repo, marker)):
65
+ return [_sys.executable, "-m", "pytest"], "pytest"
66
+ for cfg in ("pyproject.toml", "setup.cfg"):
67
+ if os.path.isfile(j(repo, cfg)) and "pytest" in _read(j(repo, cfg)).lower():
68
+ return [_sys.executable, "-m", "pytest"], "pytest"
69
+ if os.path.isdir(j(repo, "tests")):
70
+ return [_sys.executable, "-m", "pytest"], "pytest"
71
+ try:
72
+ if any(f.startswith("test_") and f.endswith(".py") for f in os.listdir(repo)):
73
+ return [_sys.executable, "-m", "pytest"], "pytest"
74
+ except Exception:
75
+ pass
76
+ # ---- Go ----
77
+ if os.path.exists(j(repo, "go.mod")):
78
+ return ["go", "test", "./..."], "go"
79
+ # ---- JS / TS: vitest, jest, or the project's own `test` script ----
80
+ pkg = _pkg(repo)
81
+ if pkg is not None:
82
+ deps = {**(pkg.get("devDependencies") or {}), **(pkg.get("dependencies") or {})}
83
+ pm = _node_pm(repo)
84
+ has_test = "test" in (pkg.get("scripts") or {})
85
+ is_vitest = ("vitest" in deps or any(os.path.exists(j(repo, f))
86
+ for f in ("vitest.config.ts", "vitest.config.js", "vitest.config.mts")))
87
+ is_jest = ("jest" in deps or any(os.path.exists(j(repo, f))
88
+ for f in ("jest.config.js", "jest.config.ts", "jest.config.mjs")))
89
+ kind = "vitest" if is_vitest else ("jest" if is_jest else "node")
90
+ if has_test: # the project's OWN command -- most faithful
91
+ return ([pm, "test"] if pm != "npm" else ["npm", "test", "--silent"]), kind
92
+ if is_vitest:
93
+ return ([pm, "exec", "vitest", "run"] if pm != "npm" else ["npx", "vitest", "run"]), "vitest"
94
+ if is_jest:
95
+ return ([pm, "exec", "jest"] if pm != "npm" else ["npx", "jest"]), "jest"
96
+ return None
97
+
98
+
99
+ def _refuse(rid, code, engine_code):
100
+ ref = refusals.build(code, engine_code=engine_code, ctx={"target": "the test suite", "run_id": rid})
101
+ return _result.make(codes.REFUSED, rid, "failing", code=code, refusal=ref)
102
+
103
+
104
+ def cmd_failing(repo: str, path: Optional[str] = None, test_command: Optional[str] = None,
105
+ timeout: int = 300) -> "_result.SubstratumResult":
106
+ repo = os.path.abspath(repo)
107
+ rid, _key = runid.compute(repo, "failing", {"path": path or ""})
108
+ if test_command:
109
+ argv, kind = shlex.split(test_command), "pytest" # user-named cmd: try the reliable parser too
110
+ else:
111
+ detected = _detect_command(repo)
112
+ if not detected:
113
+ return _refuse(rid, codes.NO_TEST_RUNNER, "no pytest / go test / vitest / jest / test script found")
114
+ argv, kind = detected
115
+ is_pytest = kind == "pytest"
116
+ run_argv = list(argv) + (["-q", "-p", "no:cacheprovider"] if is_pytest else [])
117
+ if path:
118
+ run_argv.append(path)
119
+ try:
120
+ r = subprocess.run(run_argv, cwd=repo, capture_output=True, text=True, timeout=timeout,
121
+ env={**os.environ, "TMPDIR": "/tmp", "CI": "1", "NO_COLOR": "1"})
122
+ except subprocess.TimeoutExpired:
123
+ return _refuse(rid, codes.GATE_PRECONDITION, f"the test suite did not finish within {timeout}s")
124
+ except (FileNotFoundError, PermissionError) as e:
125
+ return _refuse(rid, codes.NO_TEST_RUNNER, f"could not launch the runner ({e})")
126
+ out = (r.stdout or "") + "\n" + (r.stderr or "")
127
+ out = re.sub(r"\033\[[0-9;]*m", "", out) # strip ANSI so parsers see plain text
128
+
129
+ # reliable id parsing (pytest, go); otherwise a trustworthy failed-count from the summary
130
+ failing, count = [], 0
131
+ id_re = _ID_PARSERS.get(kind)
132
+ if id_re:
133
+ for m in id_re.finditer(out):
134
+ nid = m.group(1)
135
+ if nid not in failing:
136
+ failing.append(nid)
137
+ count = len(failing)
138
+ else:
139
+ cm = _COUNT_PARSERS.get(kind, _COUNT_PARSERS["node"]).search(out)
140
+ count = int(cm.group(1)) if cm else 0
141
+
142
+ all_pass = (count == 0) and r.returncode == 0
143
+ excerpt = ""
144
+ if not failing and not all_pass: # no ids to list -> surface the tail as evidence
145
+ excerpt = "\n".join(out.strip().splitlines()[-8:])
146
+ return _result.make(codes.PASS, rid, "failing",
147
+ data={"failing": failing, "count": count, "harness": kind,
148
+ "command": " ".join(run_argv), "all_pass": all_pass,
149
+ "excerpt": excerpt, "rc": r.returncode})