substratum-cli 0.1.0__tar.gz → 0.2.0__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.
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/PKG-INFO +1 -1
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/pyproject.toml +4 -1
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/__init__.py +3 -1
- substratum_cli-0.2.0/substratum_cli/_boot.py +164 -0
- substratum_cli-0.2.0/substratum_cli/_update.py +58 -0
- substratum_cli-0.2.0/substratum_cli/_wordmark.py +12 -0
- substratum_cli-0.2.0/substratum_cli/build_state.py +118 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/cli.py +11 -1
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/codes.py +3 -1
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/apply.py +2 -0
- substratum_cli-0.2.0/substratum_cli/commands/failing.py +149 -0
- substratum_cli-0.2.0/substratum_cli/commands/pr.py +80 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/gitio.py +16 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/product_ops.py +70 -9
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/refusals.py +10 -2
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/render.py +75 -27
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/session.py +136 -59
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/style.py +31 -6
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/PKG-INFO +1 -1
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/SOURCES.txt +5 -0
- substratum_cli-0.1.0/substratum_cli/commands/failing.py +0 -80
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/README.md +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/setup.cfg +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/README.md +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/__main__.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/_vendor/__init__.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/_vendor/_pro_test_derive.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/account.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/__init__.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/auth.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/commit.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/explain.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/fix.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/history.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/init_cmd.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/replay.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/scaffold.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/show.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/undo.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/verify.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/commands/write.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/config.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/diffs.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/engine.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/langscan.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/remote.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/result.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/run_store.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli/runid.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/dependency_links.txt +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/entry_points.txt +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/requires.txt +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/substratum_cli.egg-info/top_level.txt +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_adaptive_prior.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_alias_warrant_trace.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_bidirectional_gate_contracts.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_coherence_evolution.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_coherences.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_connection_web_api.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_cross_entity_scoping.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_data.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_de28_negation_refuse.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_definite_description_handler.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_demo_regression_catalog.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_demo_response_shape.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_e1_e2_field_alignment.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_extraction_class_write_time.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_falsification_predictions.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_gap_d_wh_normalization.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_inference.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_inflection_cvc_rule.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_inflection_irregular.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_intervention.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_invariant.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_legal_citation_handler.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_lens_noop_seam.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_markup_tag_handler.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_nl_parser.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_nl_parser_adversarial.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_noise.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_pass2_contextual.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_perspectives.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_polysemous_token_alias_guard.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_possessive_normalization.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_predicate_canon.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_projection_gate.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_projection_gate_tag_question.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_query_normalizer.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_records_answer_format.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_referent_aboutness_gate.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_referent_quality_gate.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_refuse_message_topic_neutrality.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_relation_detection.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_relaxation.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_residual_content_gate.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_reverse_lookup_subject_filter.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_sec_statement_aliases.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_span_lens.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_span_lens_query_integration.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_substrate.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_synonymy_detector.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_table_ingest.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_triple_ingest.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_two_phase_schema.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_user_asserted_aliases.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_v2_api.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_v2_api_claims.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_v2_claim_determinism.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_word_role.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_word_role_wiring.py +0 -0
- {substratum_cli-0.1.0 → substratum_cli-0.2.0}/tests/test_xbrl_doc_aliases.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "substratum-cli"
|
|
7
|
-
|
|
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
|
-
|
|
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.0"
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
out += [""]
|
|
149
|
+
if update_to: # a pip package cannot self-update; hand over the one command
|
|
150
|
+
arrow = "^" if ascii_mode else "↑"
|
|
151
|
+
out += [" " + c(GO, f"{arrow} update") + c(LAB, f" Substratum {update_to} is available")
|
|
152
|
+
+ c(CH, f" {dot} ") + c(GO, "pip install --upgrade substratum-cli")]
|
|
153
|
+
if not logged_in:
|
|
154
|
+
out += [" " + c(GO, "login") + c(LAB, " to ") + c(DAT, "fix") + c(LAB, " and ") + c(DAT, "write")
|
|
155
|
+
+ c(LAB, " code (connects the engine)")
|
|
156
|
+
+ c(CH, f" {dot} ") + c(LAB, "summarize, failing, verify work offline")]
|
|
157
|
+
out += [" " + c(GO, "help") + c(LAB, " see everything you can do")
|
|
158
|
+
+ c(CH, f" {dot} ") + c(LAB, "or just ask: ") + c(GO, ask_ex)]
|
|
159
|
+
return out
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _vis(s: str) -> int:
|
|
163
|
+
import re
|
|
164
|
+
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})
|