substratum-cli 0.2.6__tar.gz → 0.2.7__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 (118) hide show
  1. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/PKG-INFO +1 -1
  2. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/__init__.py +1 -1
  3. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/build_state.py +56 -9
  4. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/cli.py +69 -3
  5. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/codes.py +4 -2
  6. substratum_cli-0.2.7/substratum_cli/commands/apply.py +95 -0
  7. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/auth.py +1 -1
  8. substratum_cli-0.2.7/substratum_cli/commands/history.py +84 -0
  9. substratum_cli-0.2.7/substratum_cli/commands/recall.py +91 -0
  10. substratum_cli-0.2.7/substratum_cli/commands/recheck.py +89 -0
  11. substratum_cli-0.2.7/substratum_cli/commands/rules.py +45 -0
  12. substratum_cli-0.2.7/substratum_cli/identity.py +56 -0
  13. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/refusals.py +7 -0
  14. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/render.py +147 -3
  15. substratum_cli-0.2.7/substratum_cli/rulebook.py +101 -0
  16. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/run_store.py +79 -0
  17. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/session.py +337 -53
  18. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/PKG-INFO +1 -1
  19. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/SOURCES.txt +5 -0
  20. substratum_cli-0.2.6/substratum_cli/commands/apply.py +0 -51
  21. substratum_cli-0.2.6/substratum_cli/commands/history.py +0 -15
  22. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/README.md +0 -0
  23. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/pyproject.toml +0 -0
  24. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/setup.cfg +0 -0
  25. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/README.md +0 -0
  26. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/__main__.py +0 -0
  27. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_boot.py +0 -0
  28. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_update.py +0 -0
  29. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/__init__.py +0 -0
  30. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/_pro_test_derive.py +0 -0
  31. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/client_gate.py +0 -0
  32. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_wordmark.py +0 -0
  33. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/account.py +0 -0
  34. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/__init__.py +0 -0
  35. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/commit.py +0 -0
  36. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/explain.py +0 -0
  37. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/failing.py +0 -0
  38. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/fix.py +0 -0
  39. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/init_cmd.py +0 -0
  40. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/pr.py +0 -0
  41. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/replay.py +0 -0
  42. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/scaffold.py +0 -0
  43. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/show.py +0 -0
  44. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/undo.py +0 -0
  45. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/verify.py +0 -0
  46. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/write.py +0 -0
  47. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/config.py +0 -0
  48. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/diffs.py +0 -0
  49. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/engine.py +0 -0
  50. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/gitio.py +0 -0
  51. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/langscan.py +0 -0
  52. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/product_ops.py +0 -0
  53. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/remote.py +0 -0
  54. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/result.py +0 -0
  55. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/runid.py +0 -0
  56. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/style.py +0 -0
  57. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/dependency_links.txt +0 -0
  58. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/entry_points.txt +0 -0
  59. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/requires.txt +0 -0
  60. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/top_level.txt +0 -0
  61. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_adaptive_prior.py +0 -0
  62. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_alias_warrant_trace.py +0 -0
  63. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_bidirectional_gate_contracts.py +0 -0
  64. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_coherence_evolution.py +0 -0
  65. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_coherences.py +0 -0
  66. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_connection_web_api.py +0 -0
  67. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_cross_entity_scoping.py +0 -0
  68. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_data.py +0 -0
  69. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_de28_negation_refuse.py +0 -0
  70. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_definite_description_handler.py +0 -0
  71. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_demo_regression_catalog.py +0 -0
  72. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_demo_response_shape.py +0 -0
  73. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_e1_e2_field_alignment.py +0 -0
  74. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_extraction_class_write_time.py +0 -0
  75. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_falsification_predictions.py +0 -0
  76. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_gap_d_wh_normalization.py +0 -0
  77. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inference.py +0 -0
  78. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inflection_cvc_rule.py +0 -0
  79. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inflection_irregular.py +0 -0
  80. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_intervention.py +0 -0
  81. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_invariant.py +0 -0
  82. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_legal_citation_handler.py +0 -0
  83. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_lens_noop_seam.py +0 -0
  84. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_markup_tag_handler.py +0 -0
  85. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_nl_parser.py +0 -0
  86. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_nl_parser_adversarial.py +0 -0
  87. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_noise.py +0 -0
  88. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_pass2_contextual.py +0 -0
  89. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_perspectives.py +0 -0
  90. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_polysemous_token_alias_guard.py +0 -0
  91. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_possessive_normalization.py +0 -0
  92. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_predicate_canon.py +0 -0
  93. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_projection_gate.py +0 -0
  94. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_projection_gate_tag_question.py +0 -0
  95. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_query_normalizer.py +0 -0
  96. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_records_answer_format.py +0 -0
  97. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_referent_aboutness_gate.py +0 -0
  98. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_referent_quality_gate.py +0 -0
  99. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_refuse_message_topic_neutrality.py +0 -0
  100. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_relation_detection.py +0 -0
  101. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_relaxation.py +0 -0
  102. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_residual_content_gate.py +0 -0
  103. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_reverse_lookup_subject_filter.py +0 -0
  104. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_sec_statement_aliases.py +0 -0
  105. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_span_lens.py +0 -0
  106. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_span_lens_query_integration.py +0 -0
  107. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_substrate.py +0 -0
  108. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_synonymy_detector.py +0 -0
  109. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_table_ingest.py +0 -0
  110. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_triple_ingest.py +0 -0
  111. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_two_phase_schema.py +0 -0
  112. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_user_asserted_aliases.py +0 -0
  113. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_api.py +0 -0
  114. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_api_claims.py +0 -0
  115. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_claim_determinism.py +0 -0
  116. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_word_role.py +0 -0
  117. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_word_role_wiring.py +0 -0
  118. {substratum_cli-0.2.6 → substratum_cli-0.2.7}/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.6
3
+ Version: 0.2.7
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.6"
8
+ __version__ = "0.2.7"
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import json
10
10
  import os
11
- import time
12
11
  from typing import Optional
13
12
 
14
13
  from . import run_store
@@ -19,14 +18,12 @@ def _path(repo: str) -> str:
19
18
 
20
19
 
21
20
  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
21
+ """Append one event. kind: 'query' | 'decision' | 'apply' | 'commit'. Never raises.
22
+
23
+ Delegates to run_store.append_event, the single ledger writer, so apply/commit etc. carry actor +
24
+ session attribution when session state is on (logged out they stay byte-identical to prior
25
+ releases -- identity.stamp() is empty)."""
26
+ run_store.append_event(repo, kind, ts=ts, **fields)
30
27
 
31
28
 
32
29
  def _events(repo: str) -> list:
@@ -95,6 +92,55 @@ def recall_decision(repo: str, intent: str, sig: str, into: str = None) -> str:
95
92
  return got
96
93
 
97
94
 
95
+ def sessions(repo: str) -> list:
96
+ """PS0b: distinct working sessions from the ledger: [{session, actor, events, first, last}],
97
+ oldest->newest. A session groups the runs/acts of one working context (a REPL lifetime, or a
98
+ burst of one-shots sharing SUBSTRATUM_SESSION_ID) so a build is legible per session and resumable
99
+ (`substratum log --session <id>`)."""
100
+ agg: dict = {}
101
+ for e in _events(repo):
102
+ sid = e.get("session")
103
+ if not sid:
104
+ continue
105
+ t = e.get("t", 0)
106
+ a = agg.get(sid)
107
+ if a is None:
108
+ agg[sid] = {"session": sid, "actor": e.get("actor", ""), "events": 1, "first": t, "last": t}
109
+ else:
110
+ a["events"] += 1
111
+ a["first"] = min(a["first"], t)
112
+ a["last"] = max(a["last"], t)
113
+ return sorted(agg.values(), key=lambda s: s["last"])
114
+
115
+
116
+ def record_rule(repo: str, text: str, scope: str = "") -> None:
117
+ """Persist a STANDING rule/preference the user set once (e.g. 'always use pathlib', 'never catch
118
+ bare except', 'our error type is AppError'). Recalled on every future build so intent is
119
+ CUMULATIVE, not re-stated each session -- the offense against drift: a rule you set once can never
120
+ be forgotten or drifted away from 15 rounds later."""
121
+ t = " ".join((text or "").split())
122
+ if t:
123
+ record(repo, "rule", text=t, scope=(scope or ""))
124
+
125
+
126
+ def forget_rule(repo: str, text: str) -> None:
127
+ t = " ".join((text or "").split())
128
+ if t:
129
+ record(repo, "unrule", text=t)
130
+
131
+
132
+ def rules(repo: str) -> list:
133
+ """Active standing rules: every 'rule' not later 'unrule'd, most-recent first, deduped by text.
134
+ Each entry carries who set it + when (attribution) when session state was on."""
135
+ active: dict = {}
136
+ for e in _events(repo):
137
+ if e.get("kind") == "rule" and e.get("text"):
138
+ active[e["text"]] = e
139
+ elif e.get("kind") == "unrule" and e.get("text"):
140
+ active.pop(e["text"], None)
141
+ return list(reversed(list(active.values())))
142
+
143
+
98
144
  def standing(repo: str) -> dict:
99
145
  """The welcome-back summary a session shows on open, so the persistent state is FELT, not just
100
146
  queryable: how much has been built, the last run, any verified work still waiting to be applied,
@@ -115,4 +161,5 @@ def standing(repo: str) -> dict:
115
161
  "unapplied": unapplied[:3],
116
162
  "unapplied_total": len(unapplied),
117
163
  "decisions": decision_count(repo),
164
+ "rules": len(rules(repo)),
118
165
  }
@@ -52,6 +52,15 @@ def build_parser() -> argparse.ArgumentParser:
52
52
  ("scaffold-test", "emit a runnable failing test pinning a target (the flip engine)"),
53
53
  ("summarize", "describe the codebase in plain English (grounded in real symbols)"),
54
54
  ("runs", "run history + the refusal->verified conversion ledger (the proof store)"),
55
+ ("log", "the attributed decision timeline (who/when/verdict/target); filterable (pure read)"),
56
+ ("why", "the decisions that shaped a file/symbol, newest first + provenance (pure read)"),
57
+ ("blame-decision", "which verified decision last touched a line (reasoning-blame; pure read)"),
58
+ ("recheck", "re-verify a past decision against current code (regression sentinel; --suite)"),
59
+ ("remember", "record a standing rule/preference for this repo (recalled every build)"),
60
+ ("rules", "list the active standing rules (who set each, when)"),
61
+ ("forget", "retire a standing rule"),
62
+ ("sessions", "the working sessions of this build (who, how many decisions, when)"),
63
+ ("gc", "compact: keep the decision index forever, drop heavy logs older than --keep-days"),
55
64
  ("undo", "reverse-apply a run (the inverse of apply)"),
56
65
  ("commit", "proof-carrying git commit of a verified, applied run"),
57
66
  ("pr", "open a proof-carrying pull request (pushes with your git; proof in the body)"),
@@ -88,10 +97,33 @@ def build_parser() -> argparse.ArgumentParser:
88
97
  p.add_argument("--title", default=None)
89
98
  if name in ("apply", "show", "replay", "attest", "explain", "undo", "commit"):
90
99
  p.add_argument("run_id")
100
+ if name == "apply":
101
+ p.add_argument("--reverify", action="store_true",
102
+ help="re-run the gate against your current tree before writing (gold standard)")
91
103
  if name == "commit":
92
104
  p.add_argument("--message", default=None)
93
105
  if name == "explain":
94
106
  p.add_argument("--line", default=None)
107
+ if name == "why":
108
+ p.add_argument("target", help="file path or symbol to recall decisions for")
109
+ if name == "blame-decision":
110
+ p.add_argument("spec", help="path or path:line to blame the decision for")
111
+ if name == "recheck":
112
+ p.add_argument("run_id", nargs="?", default=None, help="run to re-verify (omit with --suite)")
113
+ p.add_argument("--suite", action="store_true", help="re-prove every past verified decision")
114
+ if name in ("remember", "forget"):
115
+ p.add_argument("text", help="the standing rule / preference text")
116
+ if name == "gc":
117
+ p.add_argument("--keep-days", type=int, default=90,
118
+ help="retain heavy artifacts newer than this many days (index kept forever)")
119
+ if name == "log":
120
+ p.add_argument("--limit", type=int, default=30)
121
+ p.add_argument("--verdict", default=None, help="filter by verdict (VERIFIED/REFUSED/...)")
122
+ p.add_argument("--actor", default=None, help="filter by who made the decision")
123
+ p.add_argument("--session", default=None, help="filter by session id")
124
+ p.add_argument("--target", default=None, help="filter by target substring (file/test)")
125
+ p.add_argument("--since", default=None, help="only the last N days")
126
+ p.add_argument("--mine", action="store_true", help="only your own decisions")
95
127
  return ap
96
128
 
97
129
 
@@ -106,13 +138,15 @@ def dispatch(args) -> "_result.SubstratumResult":
106
138
  return cmd_verify(args)
107
139
  if cmd == "fix":
108
140
  from .commands.fix import cmd_fix
109
- return cmd_fix(args)
141
+ from . import rulebook
142
+ return rulebook.gate(cmd_fix(args), args.repo) # final conformance layer (display transform)
110
143
  if cmd == "write":
111
144
  from .commands.write import cmd_write
112
- return cmd_write(args)
145
+ from . import rulebook
146
+ return rulebook.gate(cmd_write(args), args.repo)
113
147
  if cmd == "apply":
114
148
  from .commands.apply import cmd_apply
115
- return cmd_apply(args.repo, args.run_id)
149
+ return cmd_apply(args.repo, args.run_id, reverify=getattr(args, "reverify", False))
116
150
  if cmd == "init":
117
151
  from .commands.init_cmd import cmd_init
118
152
  return cmd_init(args.repo)
@@ -137,6 +171,38 @@ def dispatch(args) -> "_result.SubstratumResult":
137
171
  if cmd == "runs":
138
172
  from .commands.history import cmd_runs
139
173
  return cmd_runs(args.repo)
174
+ if cmd == "log":
175
+ from .commands.history import cmd_log
176
+ return cmd_log(args)
177
+ if cmd == "why":
178
+ from .commands.recall import cmd_why
179
+ return cmd_why(args.repo, args.target)
180
+ if cmd == "blame-decision":
181
+ from .commands.recall import cmd_blame_decision
182
+ return cmd_blame_decision(args.repo, args.spec)
183
+ if cmd == "recheck":
184
+ from .commands.recheck import cmd_recheck, cmd_recheck_suite
185
+ if getattr(args, "suite", False):
186
+ return cmd_recheck_suite(args.repo)
187
+ if not args.run_id:
188
+ return _result.make(codes.REFUSED, "recheck", "recheck", code=codes.RUN_NOT_FOUND,
189
+ summary="recheck needs a run id or --suite")
190
+ return cmd_recheck(args.repo, args.run_id)
191
+ if cmd == "remember":
192
+ from .commands.rules import cmd_remember
193
+ return cmd_remember(args.repo, args.text)
194
+ if cmd == "forget":
195
+ from .commands.rules import cmd_forget
196
+ return cmd_forget(args.repo, args.text)
197
+ if cmd == "rules":
198
+ from .commands.rules import cmd_rules
199
+ return cmd_rules(args.repo)
200
+ if cmd == "sessions":
201
+ from .commands.history import cmd_sessions
202
+ return cmd_sessions(args.repo)
203
+ if cmd == "gc":
204
+ from .commands.history import cmd_gc
205
+ return cmd_gc(args.repo, keep_days=getattr(args, "keep_days", 90))
140
206
  if cmd == "undo":
141
207
  from .commands.undo import cmd_undo
142
208
  return cmd_undo(args.repo, args.run_id)
@@ -34,6 +34,7 @@ REPLAY_STATE_DRIFT = "REPLAY_STATE_DRIFT" # 11
34
34
  NO_DRIVER = "NO_DRIVER" # 12: no gate runner / no filetype driver
35
35
  AUTONOMY_LOCKED = "AUTONOMY_LOCKED" # 12: an L2/L3 action attempted at L1
36
36
  UNDERDETERMINED_INTENT = "UNDERDETERMINED_INTENT" # 13: candidates composed, all failed the gate
37
+ RULEBOOK_VIOLATION = "RULEBOOK_VIOLATION" # 13: verified output violates a standing rule
37
38
  FLAKY_OR_VACUOUS_WARRANT = "FLAKY_OR_VACUOUS_WARRANT" # 14: tests pass at base (nothing to fix / flaky)
38
39
  FLAKY_TEST = "FLAKY_TEST" # 14: quarantined nondeterministic test
39
40
  RUN_NOT_FOUND = "RUN_NOT_FOUND" # 11: show/explain/replay on an unknown id
@@ -43,8 +44,8 @@ NO_TEST_RUNNER = "NO_TEST_RUNNER" # 12: `failing` found no tes
43
44
  REFUSAL_CODES = (
44
45
  NO_GROUNDED_CANDIDATE, GATE_PRECONDITION, NO_WARRANT_TESTS, HUNK_CONTEXT_STALE,
45
46
  APPLY_STATE_DRIFT, REPLAY_STATE_DRIFT, NO_DRIVER, AUTONOMY_LOCKED,
46
- UNDERDETERMINED_INTENT, FLAKY_OR_VACUOUS_WARRANT, FLAKY_TEST, RUN_NOT_FOUND,
47
- NO_ENGINE, NO_TEST_RUNNER,
47
+ UNDERDETERMINED_INTENT, RULEBOOK_VIOLATION, FLAKY_OR_VACUOUS_WARRANT, FLAKY_TEST,
48
+ RUN_NOT_FOUND, NO_ENGINE, NO_TEST_RUNNER,
48
49
  )
49
50
 
50
51
  # ---- the exit-code table (closed; the machine contract) ----------------------
@@ -66,6 +67,7 @@ _REFUSAL_EXIT = {
66
67
  NO_ENGINE: 12,
67
68
  NO_TEST_RUNNER: 12,
68
69
  UNDERDETERMINED_INTENT: 13,
70
+ RULEBOOK_VIOLATION: 13,
69
71
  FLAKY_OR_VACUOUS_WARRANT: 14,
70
72
  FLAKY_TEST: 14,
71
73
  }
@@ -0,0 +1,95 @@
1
+ """`substratum apply <run-id>` -- apply a stored run's verified diff. L1 default: apply is the ONLY
2
+ writer. The write is a deliberate ceremony (PS2d), built around what an engineer actually expects
3
+ before code lands: see exactly what changes, know it is still valid against the tree RIGHT NOW (not a
4
+ stale proof), see that it was proven, know it is reversible, and know it is recorded. Refuses
5
+ APPLY_STATE_DRIFT if HEAD moved or the diff no longer applies cleanly to the current tree."""
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import subprocess
10
+ import tempfile
11
+
12
+ from .. import codes
13
+ from .. import gitio
14
+ from .. import refusals
15
+ from .. import result as _result
16
+ from .. import run_store
17
+
18
+
19
+ def _proof_of(res) -> "dict | None":
20
+ """The stored correctness proof, surfaced at apply time so the write is transparently grounded."""
21
+ g = res.gate
22
+ if not g or not g.base_status:
23
+ return None
24
+ return {"base": g.base_status, "patched": g.patched_status, "tests": list(g.test_ids),
25
+ "kind": g.kind}
26
+
27
+
28
+ def cmd_apply(repo: str, run_id: str, reverify: bool = False) -> "_result.SubstratumResult":
29
+ repo = os.path.abspath(repo)
30
+ loaded = run_store.load_run(repo, run_id)
31
+ if loaded is None:
32
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.RUN_NOT_FOUND,
33
+ refusal=refusals.build(codes.RUN_NOT_FOUND,
34
+ ctx={"target": run_id, "run_id": run_id}))
35
+ res, inputs = loaded
36
+ recorded_head = (inputs.get("run_key") or {}).get("head_sha")
37
+ current_head = gitio.head_sha(repo)
38
+ if recorded_head and current_head != recorded_head:
39
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
40
+ refusal=refusals.build(codes.APPLY_STATE_DRIFT,
41
+ ctx={"target": run_id, "run_id": run_id}))
42
+ if not res.diff.strip():
43
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.NO_GROUNDED_CANDIDATE,
44
+ refusal=refusals.build(codes.NO_GROUNDED_CANDIDATE,
45
+ ctx={"target": run_id, "run_id": run_id}))
46
+
47
+ with tempfile.NamedTemporaryFile("w", suffix=".patch", delete=False) as fh:
48
+ fh.write(res.diff)
49
+ patch = fh.name
50
+ try:
51
+ # Ceremony 1 -- re-check against the tree AT THIS INSTANT (a dry run; the proof isn't stale).
52
+ chk = subprocess.run(["git", "-C", repo, "apply", "--check", patch],
53
+ capture_output=True, text=True, errors="replace")
54
+ rechecked = chk.returncode == 0
55
+ if not rechecked:
56
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
57
+ refusal=refusals.build(codes.APPLY_STATE_DRIFT,
58
+ ctx={"target": run_id, "run_id": run_id}))
59
+
60
+ # Ceremony 2 (opt-in gold standard) -- re-run the gate against the current tree, live.
61
+ reverified = None
62
+ if reverify and res.gate and res.gate.test_ids:
63
+ from .verify import run_verify
64
+ vr = run_verify(repo, res.diff, tuple(res.gate.test_ids))
65
+ reverified = vr.verdict
66
+ if vr.verdict not in (codes.PASS, codes.VERIFIED):
67
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
68
+ refusal=refusals.build(
69
+ codes.APPLY_STATE_DRIFT, ctx={"target": run_id, "run_id": run_id},
70
+ why="the fix no longer passes the gate against your current tree; "
71
+ "not writing a stale proof."))
72
+
73
+ # The write (unchanged core: index apply, 3-way fallback).
74
+ r = subprocess.run(["git", "-C", repo, "apply", "--index", patch],
75
+ capture_output=True, text=True, errors="replace")
76
+ if r.returncode != 0:
77
+ r2 = subprocess.run(["git", "-C", repo, "apply", "--3way", patch],
78
+ capture_output=True, text=True, errors="replace")
79
+ if r2.returncode != 0:
80
+ return _result.make(codes.REFUSED, run_id, "apply", code=codes.APPLY_STATE_DRIFT,
81
+ refusal=refusals.build(codes.APPLY_STATE_DRIFT,
82
+ ctx={"target": run_id, "run_id": run_id}))
83
+ finally:
84
+ os.unlink(patch)
85
+
86
+ # Ceremony 3 -- record the effectful decision (who / against-what-base / with-what-proof).
87
+ from .. import build_state
88
+ build_state.record(repo, "apply", run_id=run_id, files=list(res.files),
89
+ base=current_head, effect="write")
90
+ return _result.make(codes.PASS, run_id, "apply", files=res.files,
91
+ data={"applied": list(res.files),
92
+ "rechecked": "applies cleanly to your working tree",
93
+ "head_unchanged": bool(recorded_head) and current_head == recorded_head,
94
+ "reverified": reverified, "proof": _proof_of(res),
95
+ "reversible": f"substratum undo {run_id}"})
@@ -32,6 +32,6 @@ def cmd_whoami() -> int:
32
32
  if account.is_logged_in():
33
33
  sys.stdout.write(f" {account.github_login() or 'logged in'} · {account.server()}\n")
34
34
  else:
35
- sys.stdout.write(" not logged in run substratum login to use the hosted engine "
35
+ sys.stdout.write(" not logged in -- run substratum login to use the hosted engine "
36
36
  "(local commands work offline)\n")
37
37
  return 0
@@ -0,0 +1,84 @@
1
+ """`runs` / `log` -- the proof store made visible: the newest runs (every VERIFIED and every
2
+ REFUSED) plus the refusal->VERIFIED conversion ledger. This is the persistent, proof-carrying
3
+ memory that no LLM tool has, surfaced in the terminal."""
4
+ from __future__ import annotations
5
+
6
+ import time
7
+
8
+ from .. import build_state, codes, identity, run_store, runid
9
+ from .. import result as _result
10
+
11
+
12
+ def cmd_sessions(repo: str) -> "_result.SubstratumResult":
13
+ """PS0b: the working sessions of this build -- who, how many decisions, when. Resume any with
14
+ `log --session <id>`."""
15
+ ss = build_state.sessions(repo)
16
+ rid, _key = runid.compute(repo, "sessions", {"n": len(ss)})
17
+ return _result.make(codes.PASS, rid, "sessions",
18
+ data={"sessions": list(reversed(ss)), # newest first for display
19
+ "session_on": identity.session_state_on()})
20
+
21
+
22
+ def cmd_gc(repo: str, keep_days: int = 90) -> "_result.SubstratumResult":
23
+ """PS4b: keep the decision index forever; GC heavy gate logs + patches older than keep_days."""
24
+ stats = run_store.compact(repo, keep_days)
25
+ rid, _key = runid.compute(repo, "gc", {"keep_days": keep_days})
26
+ return _result.make(codes.PASS, rid, "gc",
27
+ data={"keep_days": keep_days, **stats})
28
+
29
+
30
+ def cmd_runs(repo: str, limit: int = 15) -> "_result.SubstratumResult":
31
+ runs = run_store.list_runs(repo, limit)
32
+ refn, conv = run_store.refusal_stats(repo, window_days=36500) # all-time ledger
33
+ rid, _key = runid.compute(repo, "runs", {"n": len(runs)})
34
+ return _result.make(codes.PASS, rid, "runs",
35
+ data={"runs": runs, "ledger": {"refusals": refn, "converted": conv}})
36
+
37
+
38
+ def _matches(e: dict, args, me: str) -> bool:
39
+ """PS1a filters over the attributed timeline. All pure read; a filter never re-runs anything."""
40
+ vf = getattr(args, "verdict", None)
41
+ if vf and (e.get("verdict") or "").upper() != vf.upper():
42
+ return False
43
+ af = getattr(args, "actor", None)
44
+ if af and (e.get("actor") or "") != af:
45
+ return False
46
+ sf = getattr(args, "session", None)
47
+ if sf and (e.get("session") or "") != sf:
48
+ return False
49
+ if getattr(args, "mine", False) and (e.get("actor") or "") != me:
50
+ return False
51
+ tf = getattr(args, "target", None)
52
+ if tf and tf.lower() not in (e.get("target") or "").lower():
53
+ return False
54
+ since = getattr(args, "since", None)
55
+ if since:
56
+ try:
57
+ if e.get("t", 0) < time.time() - float(since) * 86400.0:
58
+ return False
59
+ except (TypeError, ValueError):
60
+ pass
61
+ return True
62
+
63
+
64
+ def cmd_log(args) -> "_result.SubstratumResult":
65
+ """PS1a: the attributed, filterable decision timeline (pure read; never re-executes). Logged in ->
66
+ the attributed 'run' events (who/when/verdict/target/effect), filtered. Logged out / no ledger ->
67
+ the base run history, unattributed (identical to `runs`)."""
68
+ repo = args.repo
69
+ limit = getattr(args, "limit", None) or 30
70
+ me = identity.actor()
71
+ runs = [e for e in run_store.read_activity(repo) if e.get("kind") == "run"]
72
+ attributed = bool(runs)
73
+ if attributed:
74
+ entries = [e for e in runs if _matches(e, args, me)]
75
+ entries.sort(key=lambda e: e.get("t", 0), reverse=True)
76
+ entries = entries[:limit]
77
+ else:
78
+ entries = run_store.list_runs(repo, limit) # base history when session state is off
79
+ refn, conv = run_store.refusal_stats(repo, window_days=36500)
80
+ rid, _key = runid.compute(repo, "log", {"n": len(entries)})
81
+ return _result.make(codes.PASS, rid, "log",
82
+ data={"timeline": entries, "attributed": attributed,
83
+ "ledger": {"refusals": refn, "converted": conv},
84
+ "session_on": identity.session_state_on()})
@@ -0,0 +1,91 @@
1
+ """PS1b/PS1c: recall the DECISIONS behind code.
2
+
3
+ `why <file/symbol>` -- every decision that shaped a target, newest first, with its provenance.
4
+ `blame-decision <f:line>`-- the decision that last touched a specific line (git-blame for reasoning).
5
+
6
+ Both are PURE READ over the activity ledger + the proof store: they never re-execute and never write.
7
+ When session state is off (logged out) there is no attributed timeline, so they surface a login hint
8
+ and fall back to whatever base records exist.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from .. import codes, diffs, identity, run_store, runid
13
+ from .. import result as _result
14
+
15
+
16
+ def _runs_touching(repo: str, needle: str) -> list:
17
+ """'run' events whose target or files mention `needle` (case-insensitive substring), in ledger
18
+ order (oldest->newest)."""
19
+ n = (needle or "").lower()
20
+ out = []
21
+ for e in run_store.read_activity(repo):
22
+ if e.get("kind") != "run":
23
+ continue
24
+ files = e.get("files") or []
25
+ if n in (e.get("target") or "").lower() or any(n in (f or "").lower() for f in files):
26
+ out.append(e)
27
+ return out
28
+
29
+
30
+ def _enrich(repo: str, e: dict) -> dict:
31
+ """Fold the stored proof's provenance + summary into a timeline entry."""
32
+ prov, summary = [], ""
33
+ loaded = run_store.load_run(repo, e.get("run_id"))
34
+ if loaded:
35
+ res, _inp = loaded
36
+ summary = res.summary or ""
37
+ if res.provenance:
38
+ prov = list(res.provenance.entries)
39
+ return {"run_id": e.get("run_id"), "verdict": e.get("verdict"), "operation": e.get("operation"),
40
+ "actor": e.get("actor"), "session": e.get("session"), "t": e.get("t"),
41
+ "effect": e.get("effect"), "files": e.get("files") or [],
42
+ "provenance": prov, "summary": summary}
43
+
44
+
45
+ def cmd_why(repo: str, target: str) -> "_result.SubstratumResult":
46
+ matched = _runs_touching(repo, target)
47
+ decisions = [_enrich(repo, e) for e in matched][-25:]
48
+ decisions.reverse() # newest first
49
+ rid, _k = runid.compute(repo, "why", {"target": target})
50
+ return _result.make(codes.PASS, rid, "why",
51
+ data={"target": target, "decisions": decisions,
52
+ "session_on": identity.session_state_on()})
53
+
54
+
55
+ def _diff_touches(diff_text: str, path: str, line: int) -> bool:
56
+ """Does any hunk for `path` cover new-side `line`?"""
57
+ if not diff_text:
58
+ return False
59
+ try:
60
+ for fd in diffs.parse(diff_text):
61
+ if not (fd.path == path or fd.path.endswith("/" + path) or path.endswith("/" + fd.path)):
62
+ continue
63
+ for h in fd.hunks:
64
+ if h.new_start <= line < h.new_start + max(h.new_len, 1):
65
+ return True
66
+ except Exception:
67
+ return False
68
+ return False
69
+
70
+
71
+ def cmd_blame_decision(repo: str, spec: str) -> "_result.SubstratumResult":
72
+ """spec = 'path' or 'path:line'. The most recent decision that touched that line (or, with no
73
+ line, the file). Reasoning-blame: not who committed it, but which verified decision established
74
+ it and why."""
75
+ path, _, lno = (spec or "").partition(":")
76
+ line = int(lno) if lno.isdigit() else None
77
+ candidates = _runs_touching(repo, path)
78
+ hit = None
79
+ for e in reversed(candidates): # newest first
80
+ if line is None:
81
+ hit = e
82
+ break
83
+ loaded = run_store.load_run(repo, e.get("run_id"))
84
+ if loaded and _diff_touches(loaded[0].diff, path, line):
85
+ hit = e
86
+ break
87
+ blame = _enrich(repo, hit) if hit else None
88
+ rid, _k = runid.compute(repo, "blame", {"spec": spec})
89
+ return _result.make(codes.PASS, rid, "blame",
90
+ data={"spec": spec, "path": path, "line": line, "blame": blame,
91
+ "session_on": identity.session_state_on()})
@@ -0,0 +1,89 @@
1
+ """PS2a/PS2c: `recheck` -- re-verify a past decision against the CURRENT code (the regression /
2
+ conformance sentinel). Distinct from `replay`, which proves determinism when nothing changed:
3
+ recheck EXPECTS the world to have moved and reports the delta.
4
+
5
+ It RE-DERIVES the original intent against the current base -- a pure function of intent + base +
6
+ versions (zero-LLM, air-gapped, no tokens) -- and NEVER writes the working tree. Turning a re-
7
+ derivation into a change is always a separate, gated `apply` (PS2b: derivation vs effect stay split).
8
+
9
+ `regressed` (was VERIFIED, now not) is the alarm: zero-wrong extended across TIME. `recheck --suite`
10
+ re-proves every past verified decision so an engine/library/version bump can never silently regress
11
+ what your repo already established.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from .. import codes, refusals, run_store, runid
16
+ from .. import result as _result
17
+
18
+ _OK = ("VERIFIED", "PASS")
19
+
20
+
21
+ def classify_delta(base_verdict: str, now_verdict: str, base_diff: str, now_diff: str) -> str:
22
+ """Pure: the status of a re-derivation vs its stored decision. 'regressed' is the alarm."""
23
+ was, now = base_verdict in _OK, now_verdict in _OK
24
+ if was and not now:
25
+ return "regressed"
26
+ if not was and now:
27
+ return "recovered"
28
+ if base_verdict == now_verdict:
29
+ return "stable" if (base_diff or "") == (now_diff or "") else "drifted"
30
+ return "changed"
31
+
32
+
33
+ def _reexecute(repo: str, op: str, inputs: dict):
34
+ """Re-derive the ORIGINAL intent against the CURRENT base. Producers only (fix/write); NEVER
35
+ writes the tree (op_* return a proposal + store a record; apply is the only writer). Returns a
36
+ fresh SubstratumResult, or None if this op cannot be re-derived (informational ops)."""
37
+ inp = inputs.get("inputs") or {}
38
+ if op == "fix" and inp.get("failing_tests"):
39
+ from .. import product_ops
40
+ return product_ops.op_fix_from_failing_test(
41
+ repo, list(inp["failing_tests"]), intent=inp.get("intent", ""),
42
+ test_command=inp.get("test_command"))
43
+ if op == "write" and inp.get("sig"):
44
+ from .. import product_ops
45
+ return product_ops.op_write_from_intent(
46
+ repo, inp.get("intent", ""), inp["sig"], into=inp.get("into"),
47
+ prefer_purpose=inp.get("prefer"))
48
+ return None
49
+
50
+
51
+ def cmd_recheck(repo: str, run_id: str) -> "_result.SubstratumResult":
52
+ loaded = run_store.load_run(repo, run_id)
53
+ if loaded is None:
54
+ return _result.make(codes.REFUSED, run_id, "recheck", code=codes.RUN_NOT_FOUND,
55
+ refusal=refusals.build(codes.RUN_NOT_FOUND,
56
+ ctx={"target": run_id, "run_id": run_id}))
57
+ stored, inputs = loaded
58
+ re_res = _reexecute(repo, stored.operation, inputs)
59
+ if re_res is None:
60
+ return _result.make(codes.REFUSED, run_id, "recheck", code=codes.GATE_PRECONDITION,
61
+ refusal=refusals.build(codes.GATE_PRECONDITION,
62
+ ctx={"target": run_id, "run_id": run_id},
63
+ why=f"a {stored.operation} run cannot be re-derived"))
64
+ status = classify_delta(stored.verdict, re_res.verdict, stored.diff, re_res.diff)
65
+ verdict = codes.FAIL if status == "regressed" else codes.PASS
66
+ rid, _k = runid.compute(repo, "recheck", {"of": run_id})
67
+ return _result.make(verdict, rid, "recheck",
68
+ data={"of": run_id, "status": status, "base_verdict": stored.verdict,
69
+ "now_verdict": re_res.verdict, "now_run_id": re_res.run_id,
70
+ "diff_changed": (stored.diff or "") != (re_res.diff or "")})
71
+
72
+
73
+ def cmd_recheck_suite(repo: str, limit: int = 500) -> "_result.SubstratumResult":
74
+ """Re-prove every past VERIFIED producer decision against current HEAD. Any regression fails the
75
+ suite. This is the sentinel: run it in CI after an engine/library bump."""
76
+ runs = run_store.list_runs(repo, limit)
77
+ verified = [r for r in runs if r.get("verdict") in _OK and r.get("operation") in ("fix", "write")]
78
+ results, regressed = [], 0
79
+ for r in verified:
80
+ res = cmd_recheck(repo, r["run_id"])
81
+ st = (res.data or {}).get("status", "?")
82
+ regressed += (st == "regressed")
83
+ results.append({"run_id": r["run_id"], "status": st,
84
+ "now_verdict": (res.data or {}).get("now_verdict")})
85
+ verdict = codes.FAIL if regressed else codes.PASS
86
+ rid, _k = runid.compute(repo, "recheck_suite", {"n": len(verified)})
87
+ return _result.make(verdict, rid, "recheck",
88
+ data={"suite": True, "checked": len(verified), "regressed": regressed,
89
+ "results": results})
@@ -0,0 +1,45 @@
1
+ """PS3c: standing rules / preferences -- intent made cumulative.
2
+
3
+ `remember "<rule>"` records a standing rule for this repo (login-gated: it's session state).
4
+ `rules` lists the active standing rules (who set each, when).
5
+ `forget "<rule>"` retires a rule.
6
+
7
+ A rule set once is recalled on every future build, so the user never re-states intent and the system
8
+ can never drift off an established preference. Applying rules INTO derivation as grounding is the
9
+ engine-side lever (PS3b); this is the record + recall + surface side. Pure client, never touches the
10
+ gate.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from .. import build_state, codes, identity, runid
15
+ from .. import result as _result
16
+
17
+
18
+ def _snapshot(repo: str, extra: dict = None) -> dict:
19
+ d = {"rules": build_state.rules(repo), "session_on": identity.session_state_on()}
20
+ if extra:
21
+ d.update(extra)
22
+ return d
23
+
24
+
25
+ def cmd_remember(repo: str, text: str) -> "_result.SubstratumResult":
26
+ rid, _k = runid.compute(repo, "remember", {"text": text})
27
+ if not identity.session_state_on():
28
+ return _result.make(codes.REFUSED, rid, "rules", code=codes.AUTONOMY_LOCKED,
29
+ data=_snapshot(repo, {"recorded": None}))
30
+ build_state.record_rule(repo, text)
31
+ return _result.make(codes.PASS, rid, "rules",
32
+ data=_snapshot(repo, {"recorded": " ".join((text or "").split())}))
33
+
34
+
35
+ def cmd_forget(repo: str, text: str) -> "_result.SubstratumResult":
36
+ rid, _k = runid.compute(repo, "forget", {"text": text})
37
+ if identity.session_state_on():
38
+ build_state.forget_rule(repo, text)
39
+ return _result.make(codes.PASS, rid, "rules",
40
+ data=_snapshot(repo, {"forgot": " ".join((text or "").split())}))
41
+
42
+
43
+ def cmd_rules(repo: str) -> "_result.SubstratumResult":
44
+ rid, _k = runid.compute(repo, "rules", {})
45
+ return _result.make(codes.PASS, rid, "rules", data=_snapshot(repo))