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.
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/PKG-INFO +1 -1
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/__init__.py +1 -1
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/build_state.py +56 -9
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/cli.py +69 -3
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/codes.py +4 -2
- substratum_cli-0.2.7/substratum_cli/commands/apply.py +95 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/auth.py +1 -1
- substratum_cli-0.2.7/substratum_cli/commands/history.py +84 -0
- substratum_cli-0.2.7/substratum_cli/commands/recall.py +91 -0
- substratum_cli-0.2.7/substratum_cli/commands/recheck.py +89 -0
- substratum_cli-0.2.7/substratum_cli/commands/rules.py +45 -0
- substratum_cli-0.2.7/substratum_cli/identity.py +56 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/refusals.py +7 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/render.py +147 -3
- substratum_cli-0.2.7/substratum_cli/rulebook.py +101 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/run_store.py +79 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/session.py +337 -53
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/PKG-INFO +1 -1
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/SOURCES.txt +5 -0
- substratum_cli-0.2.6/substratum_cli/commands/apply.py +0 -51
- substratum_cli-0.2.6/substratum_cli/commands/history.py +0 -15
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/README.md +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/pyproject.toml +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/setup.cfg +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/README.md +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/__main__.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_boot.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_update.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/__init__.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/_pro_test_derive.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_vendor/client_gate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/_wordmark.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/account.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/__init__.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/commit.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/explain.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/failing.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/fix.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/init_cmd.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/pr.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/replay.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/scaffold.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/show.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/undo.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/verify.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/commands/write.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/config.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/diffs.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/engine.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/gitio.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/langscan.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/product_ops.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/remote.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/result.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/runid.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli/style.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/dependency_links.txt +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/entry_points.txt +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/requires.txt +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/substratum_cli.egg-info/top_level.txt +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_adaptive_prior.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_alias_warrant_trace.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_bidirectional_gate_contracts.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_coherence_evolution.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_coherences.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_connection_web_api.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_cross_entity_scoping.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_data.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_de28_negation_refuse.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_definite_description_handler.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_demo_regression_catalog.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_demo_response_shape.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_e1_e2_field_alignment.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_extraction_class_write_time.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_falsification_predictions.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_gap_d_wh_normalization.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inference.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inflection_cvc_rule.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_inflection_irregular.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_intervention.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_invariant.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_legal_citation_handler.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_lens_noop_seam.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_markup_tag_handler.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_nl_parser.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_nl_parser_adversarial.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_noise.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_pass2_contextual.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_perspectives.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_polysemous_token_alias_guard.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_possessive_normalization.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_predicate_canon.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_projection_gate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_projection_gate_tag_question.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_query_normalizer.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_records_answer_format.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_referent_aboutness_gate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_referent_quality_gate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_refuse_message_topic_neutrality.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_relation_detection.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_relaxation.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_residual_content_gate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_reverse_lookup_subject_filter.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_sec_statement_aliases.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_span_lens.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_span_lens_query_integration.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_substrate.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_synonymy_detector.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_table_ingest.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_triple_ingest.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_two_phase_schema.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_user_asserted_aliases.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_api.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_api_claims.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_v2_claim_determinism.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_word_role.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_word_role_wiring.py +0 -0
- {substratum_cli-0.2.6 → substratum_cli-0.2.7}/tests/test_xbrl_doc_aliases.py +0 -0
|
@@ -5,4 +5,4 @@ Import budget: this package is stdlib-only; the ONLY module reaching into backen
|
|
|
5
5
|
"""
|
|
6
6
|
# SINGLE SOURCE OF TRUTH for the release version. pyproject reads this (dynamic version), the boot shows
|
|
7
7
|
# it, and the update-check compares against it. To cut a release, bump ONLY this line, then publish.
|
|
8
|
-
__version__ = "0.2.
|
|
8
|
+
__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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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))
|