rung-cli 0.1.2__py3-none-any.whl

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 (64) hide show
  1. rung/__init__.py +3 -0
  2. rung/__main__.py +3 -0
  3. rung/agents/__init__.py +15 -0
  4. rung/agents/hooks.py +66 -0
  5. rung/agents/install.py +120 -0
  6. rung/analysis/__init__.py +1 -0
  7. rung/analysis/plan.py +165 -0
  8. rung/analysis/report.py +125 -0
  9. rung/analysis/survey.py +319 -0
  10. rung/checks/__init__.py +1 -0
  11. rung/checks/base.py +59 -0
  12. rung/checks/command.py +122 -0
  13. rung/checks/deps.py +230 -0
  14. rung/checks/findings.py +153 -0
  15. rung/checks/formal.py +112 -0
  16. rung/checks/presets.py +96 -0
  17. rung/checks/prove.py +125 -0
  18. rung/checks/registry.py +35 -0
  19. rung/checks/runner.py +161 -0
  20. rung/checks/scan.py +152 -0
  21. rung/checks/shortcuts.py +109 -0
  22. rung/cli/__init__.py +157 -0
  23. rung/cli/commands/__init__.py +13 -0
  24. rung/cli/commands/common.py +41 -0
  25. rung/cli/commands/draft.py +214 -0
  26. rung/cli/commands/manage.py +112 -0
  27. rung/cli/commands/run.py +114 -0
  28. rung/cli/commands/setup.py +174 -0
  29. rung/cli/output.py +54 -0
  30. rung/core/__init__.py +1 -0
  31. rung/core/config.py +278 -0
  32. rung/core/errors.py +9 -0
  33. rung/core/git.py +236 -0
  34. rung/core/laws.py +124 -0
  35. rung/core/ledger.py +225 -0
  36. rung/core/proc.py +58 -0
  37. rung/core/redact.py +36 -0
  38. rung/drafting/__init__.py +1 -0
  39. rung/drafting/common.py +86 -0
  40. rung/drafting/lean.py +117 -0
  41. rung/drafting/property.py +339 -0
  42. rung/drafting/providers.py +294 -0
  43. rung/drafting/runtime.py +186 -0
  44. rung/drafting/sandbox.py +63 -0
  45. rung/drafting/tla.py +97 -0
  46. rung/engines/__init__.py +1 -0
  47. rung/engines/drt.py +123 -0
  48. rung/engines/lean.py +209 -0
  49. rung/engines/mutate.py +138 -0
  50. rung/engines/sarif.py +223 -0
  51. rung/engines/shortcuts.py +426 -0
  52. rung/engines/tla.py +199 -0
  53. rung/guides/__init__.py +20 -0
  54. rung/guides/lean.md +64 -0
  55. rung/guides/property.md +48 -0
  56. rung/guides/rules.md +15 -0
  57. rung/guides/runtime.md +94 -0
  58. rung/guides/tla.md +58 -0
  59. rung/guides/verify.md +14 -0
  60. rung_cli-0.1.2.dist-info/METADATA +296 -0
  61. rung_cli-0.1.2.dist-info/RECORD +64 -0
  62. rung_cli-0.1.2.dist-info/WHEEL +4 -0
  63. rung_cli-0.1.2.dist-info/entry_points.txt +2 -0
  64. rung_cli-0.1.2.dist-info/licenses/LICENSE +202 -0
rung/cli/__init__.py ADDED
@@ -0,0 +1,157 @@
1
+ """`rung` command line: argument parsing and error handling. Commands live in `commands`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from rung import __version__
10
+ from rung.checks.presets import PRESETS
11
+ from rung.cli import commands as c
12
+ from rung.cli.output import Output
13
+ from rung.core.config import CHECK_KINDS, ConfigError
14
+ from rung.core.errors import RungError, ToolMissing
15
+ from rung.core.git import GitError
16
+ from rung.drafting import providers
17
+ from rung.engines.sarif import LEVELS
18
+
19
+ EXIT_USAGE, EXIT_TOOL, EXIT_GIT = 2, 3, 4
20
+
21
+ DESCRIPTION = """\
22
+ Rung keeps a ledger of what has actually been verified in a codebase, and against
23
+ which exact code. Checks range from your test suite and security scanners to TLA+
24
+ model checking and Lean proofs; laws in LAWS.md say what must never break.
25
+
26
+ exit codes: 0 ok · 1 a check or gate failed · 2 configuration or usage error ·
27
+ 3 a required tool is missing · 4 git error"""
28
+
29
+
30
+ def _common_options(*, defaults: bool) -> argparse.ArgumentParser:
31
+ """Options accepted before or after the subcommand.
32
+
33
+ The subcommand copies use SUPPRESS defaults so `rung -C dir check` is not
34
+ overwritten by the subcommand's own default.
35
+ """
36
+ parser = argparse.ArgumentParser(add_help=False)
37
+ suppress = not defaults
38
+ parser.add_argument("-C", "--dir", help="project directory (default: current)",
39
+ default=argparse.SUPPRESS if suppress else ".") # fmt: skip
40
+ parser.add_argument("--json", action="store_true", help="print machine-readable JSON",
41
+ default=argparse.SUPPRESS if suppress else False) # fmt: skip
42
+ parser.add_argument("--no-color", action="store_true", help="disable colour",
43
+ default=argparse.SUPPRESS if suppress else False) # fmt: skip
44
+ return parser
45
+
46
+
47
+ def build_parser() -> argparse.ArgumentParser:
48
+ common = _common_options(defaults=False)
49
+ p = argparse.ArgumentParser(
50
+ prog="rung",
51
+ description=DESCRIPTION,
52
+ formatter_class=argparse.RawDescriptionHelpFormatter,
53
+ parents=[_common_options(defaults=True)],
54
+ )
55
+ p.add_argument("--version", action="version", version=f"rung {__version__}")
56
+ sub = p.add_subparsers(dest="command", required=True, metavar="<command>")
57
+
58
+ def add(name: str, func, help_text: str) -> argparse.ArgumentParser:
59
+ s = sub.add_parser(name, help=help_text, description=help_text, parents=[common])
60
+ s.set_defaults(func=func)
61
+ return s
62
+
63
+ s = add("init", c.cmd_init, "survey the repo, choose a model provider, write rung.toml")
64
+ s.add_argument("--provider", choices=list(providers.PROVIDERS))
65
+ s.add_argument("--model", help="model for drafting, passed to the provider (e.g. sonnet, opus, gpt-6.1-sol); "
66
+ "empty uses the CLI's own default") # fmt: skip
67
+ add("plan", c.cmd_plan, "recommend checks for this repo")
68
+ add("doctor", c.cmd_doctor, "show which checkers, scanners and model CLIs are available")
69
+ s = add("agents", c.cmd_agents, "install the agent skill and AGENTS.md rules (and enforcement hooks)")
70
+ s.add_argument("--hooks", action="store_true", help="also install a Claude Code Stop hook that blocks stale work")
71
+ s = add("hook", c.cmd_hook, "entry point for agent hooks (reads the hook payload on stdin)")
72
+ s.add_argument("event", choices=["stop"])
73
+
74
+ s = add("check", c.cmd_check, "run checks and record verdicts")
75
+ s.add_argument("ids", nargs="*", help="check ids (default: all)")
76
+ s.add_argument("--changed", action="store_true", help="only checks touched by this branch's changes")
77
+ s.add_argument("--base", help="branch to compare with (default: the repository's default branch)")
78
+ s.add_argument("--ci", action="store_true", help="skip checks that are already fresh")
79
+ s = add("status", c.cmd_status, "show which verdicts are fresh, stale or failing")
80
+ s.add_argument("--ci", action="store_true", help="exit 1 unless every verdict is fresh")
81
+ s = add("report", c.cmd_report, "summarize checks, laws, new findings and fix proofs")
82
+ s.add_argument("--format", choices=["md", "json"], default="md")
83
+ s = add("prove", c.cmd_prove, "show a new test fails without the fix and passes with it")
84
+ s.add_argument("test_command", metavar="command", help="test command, e.g. 'pytest tests/test_refunds.py'")
85
+ s.add_argument("--base", help="branch without the fix (default: the repository's default branch)")
86
+ s.add_argument("--tests", nargs="*", help="test files to carry over (default: test files changed on this branch)")
87
+ s.add_argument("--expect", help="text the failing run must contain, e.g. the assertion")
88
+ s.add_argument("--cwd", default=".", help="directory to run the test command in (e.g. backend)")
89
+ s = add("ledger", c.cmd_ledger, "check the verdict files for corruption")
90
+ s.add_argument("--prune", action="store_true", help="remove verdicts of checks no longer in rung.toml")
91
+
92
+ s = add("laws", c.cmd_laws, "list laws and whether each one holds")
93
+ s.add_argument("--approve", metavar="LAW_ID", help="mark a proposed law as approved")
94
+ s = add("add", c.cmd_add, "declare a check in rung.toml")
95
+ s.add_argument("kind", choices=[k for k in CHECK_KINDS if k not in ("tla", "lean", "drt")])
96
+ s.add_argument("id")
97
+ s.add_argument("--run", help="command to run (command, custom scan; for evidence, omit it to attest instead)")
98
+ s.add_argument("--cwd", help="directory to run the command in, relative to the project root")
99
+ s.add_argument("--tool", choices=list(PRESETS), help="scanner preset (scan)")
100
+ s.add_argument("--fail-on", choices=LEVELS, help="lowest SARIF level that blocks (scan)")
101
+ s.add_argument("--require", nargs="*", help="artifact suffixes the evidence must include, e.g. .webm")
102
+ s.add_argument("--min-age-days", type=int, default=30, help="youngest acceptable new package (deps)")
103
+ s.add_argument("--covers", nargs="*", help="files whose change stales the verdict (globs)")
104
+ s = add("attest", c.cmd_attest, "record evidence an agent or person captured (evidence checks without `run`)")
105
+ s.add_argument("check")
106
+ s.add_argument("files", nargs="+", help="recordings, screenshots, transcripts")
107
+ s.add_argument("--by", required=True, help="who or what recorded it, e.g. claude-in-chrome, computer-use, codex")
108
+ s.add_argument("--note", help="what was driven and what was seen")
109
+ s = add("baseline", c.cmd_baseline, "accept a scan check's existing findings; only new ones will block")
110
+ s.add_argument("check")
111
+ s = add("waive", c.cmd_waive, "accept one finding, with a reason and an expiry")
112
+ s.add_argument("check")
113
+ s.add_argument("fingerprint")
114
+ s.add_argument("--reason", required=True)
115
+ s.add_argument("--days", type=int, default=30, help="days until the waiver expires (default 30)")
116
+ s.add_argument("--until", help="expiry date, YYYY-MM-DD")
117
+ s.add_argument("--approved-by", help="who accepts the risk (default: git user.name)")
118
+ s = add("draft", c.cmd_draft, "draft a TLA+ spec, Lean model, property tests or drive script; a checker decides")
119
+ s.add_argument("kind", choices=["tla", "lean", "property", "runtime"])
120
+ s.add_argument("files", nargs="*", help="source files to model (tla, lean) or to focus on (runtime)")
121
+ s.add_argument("--intent", help="what the code must guarantee, or (runtime) which feature to drive")
122
+ s.add_argument("--require", nargs="*", help="(runtime) evidence the script must leave, e.g. .webm for a video")
123
+ s.add_argument("--id")
124
+ s.add_argument("--out", help="where to write the result: a directory (tla, lean) or a test file (property)")
125
+ s.add_argument("--model", help="model for this draft only (default: [llm] model in rung.toml, else the CLI's)")
126
+ return p
127
+
128
+
129
+ def main(argv: list[str] | None = None) -> int:
130
+ args = build_parser().parse_args(argv)
131
+ out = Output(json_mode=args.json, color=False if args.no_color else None)
132
+ try:
133
+ return args.func(args, out)
134
+ except ConfigError as exc:
135
+ out.error(str(exc))
136
+ return EXIT_USAGE
137
+ except ToolMissing as exc:
138
+ out.error(str(exc))
139
+ return EXIT_TOOL
140
+ except GitError as exc:
141
+ out.error(str(exc))
142
+ return EXIT_GIT
143
+ except RungError as exc:
144
+ out.error(str(exc))
145
+ return c.GATE_FAILED
146
+ except KeyboardInterrupt:
147
+ out.error("interrupted")
148
+ return 130
149
+ except Exception as exc: # a bug or an unexpected input: say so briefly, not with a traceback
150
+ if os.environ.get("RUNG_DEBUG"):
151
+ raise
152
+ out.error(f"unexpected error: {type(exc).__name__}: {exc} (set RUNG_DEBUG=1 for the traceback)")
153
+ return EXIT_USAGE
154
+
155
+
156
+ if __name__ == "__main__":
157
+ sys.exit(main())
@@ -0,0 +1,13 @@
1
+ """Command implementations. Each takes parsed args and an Output, and returns an exit code."""
2
+
3
+ from rung.cli.commands.common import GATE_FAILED, OK
4
+ from rung.cli.commands.draft import cmd_draft
5
+ from rung.cli.commands.manage import cmd_add, cmd_attest, cmd_baseline, cmd_laws, cmd_waive
6
+ from rung.cli.commands.run import cmd_check, cmd_ledger, cmd_prove, cmd_report, cmd_status
7
+ from rung.cli.commands.setup import cmd_agents, cmd_doctor, cmd_hook, cmd_init, cmd_plan
8
+
9
+ __all__ = [
10
+ "GATE_FAILED", "OK", "cmd_add", "cmd_agents", "cmd_attest", "cmd_baseline", "cmd_check", "cmd_doctor", "cmd_draft",
11
+ "cmd_hook", "cmd_init", "cmd_laws", "cmd_ledger", "cmd_plan", "cmd_prove", "cmd_report", "cmd_status",
12
+ "cmd_waive",
13
+ ] # fmt: skip
@@ -0,0 +1,41 @@
1
+ """Helpers shared by the command modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from rung.checks import registry
9
+ from rung.cli.output import Output
10
+ from rung.core import git
11
+ from rung.core.config import CONFIG_NAME, Check, Config, find_root, load, save
12
+
13
+ OK, GATE_FAILED = 0, 1
14
+ OK, GATE_FAILED = 0, 1
15
+
16
+
17
+ def load_config(args) -> Config:
18
+ return load(find_root(Path(args.dir)))
19
+
20
+
21
+ def ask(question: str, default: str = "") -> str:
22
+ if not sys.stdin.isatty():
23
+ return default
24
+ suffix = f" [{default}]" if default else ""
25
+ print(f"{question}{suffix}: ", end="", file=sys.stderr, flush=True)
26
+ return input().strip() or default
27
+
28
+
29
+ def resolve_base(root: Path, base: str | None) -> str:
30
+ """A merge base commit for `base` (or the default branch)."""
31
+ return git.merge_base(root, base or git.default_base(root))
32
+
33
+
34
+ def add_check(config: Config, check: Check, out: Output) -> None:
35
+ registry.get(check.kind) # validate the kind before writing anything
36
+ replaced = any(c.id == check.id for c in config.checks)
37
+ config.checks = [c for c in config.checks if c.id != check.id] + [check]
38
+ save(config)
39
+ out.emit({"check": check.id, "kind": check.kind, "covers": check.covers, "options": check.options,
40
+ "replaced": replaced}) # fmt: skip
41
+ out.line(f"{'Updated' if replaced else 'Added'} check {check.id!r} ({check.kind}) in {CONFIG_NAME}.")
@@ -0,0 +1,214 @@
1
+ """`rung draft`: model-drafted TLA+ specs, Lean models and runtime drive scripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import tomllib
7
+ from dataclasses import replace
8
+ from pathlib import Path
9
+
10
+ from rung import agents
11
+ from rung.analysis import survey
12
+ from rung.cli.commands.common import GATE_FAILED, OK, add_check, load_config
13
+ from rung.cli.output import Output
14
+ from rung.core import laws
15
+ from rung.core.config import Check, Config, ConfigError
16
+ from rung.drafting import lean as lean_draft
17
+ from rung.drafting import property as property_draft
18
+ from rung.drafting import providers, runtime
19
+ from rung.drafting import tla as tla_draft
20
+
21
+
22
+ def drafted_by(cfg: Config, provider: providers.Provider | None = None) -> str:
23
+ """Which provider and model drafted an artifact, recorded on the check it creates.
24
+
25
+ After a draft, `provider` reports the models that actually answered (Claude Code's own report, or
26
+ Codex's configured default), so `default` is only left when the tool does not say.
27
+ """
28
+ model = provider.models() if provider else cfg.llm.model or "your CLI's default model"
29
+ return f"{cfg.llm.provider}:{model}"
30
+
31
+
32
+ def _provider(cfg: Config, args) -> providers.Provider:
33
+ """The configured provider, with `--model` applied to this draft only (rung.toml is not changed)."""
34
+ return providers.get(replace(cfg.llm, model=args.model) if args.model else cfg.llm)
35
+
36
+
37
+ def _slug(paths: list[str]) -> str:
38
+ return re.sub(r"[^a-z0-9]+", "-", Path(paths[0]).stem.lower()).strip("-") or "spec"
39
+
40
+
41
+ def _lean_exe(project: Path) -> str:
42
+ try:
43
+ lakefile = tomllib.loads((project / "lakefile.toml").read_text())
44
+ except (OSError, tomllib.TOMLDecodeError):
45
+ return ""
46
+ exes = lakefile.get("lean_exe", [])
47
+ return exes[0].get("name", "") if exes else ""
48
+
49
+
50
+ def _draft_runtime(args, cfg: Config, out: Output) -> int:
51
+ provider = _provider(cfg, args)
52
+ info = survey.survey(cfg.root)
53
+ check_id = args.id or "runtime"
54
+ out.line(
55
+ f"Drafting a runtime verification script with {drafted_by(cfg, provider)}; Rung runs it until it proves itself."
56
+ )
57
+ result = runtime.draft_runtime(
58
+ provider,
59
+ cfg.root,
60
+ info,
61
+ focus=args.files,
62
+ check_id=check_id,
63
+ intent=args.intent or "",
64
+ require=tuple(args.require or ()),
65
+ )
66
+ out.emit({"ok": result.ok, "message": result.message, "rounds": result.rounds, "covers": result.covers,
67
+ "files": [str(f.relative_to(cfg.root)) for f in result.files], "evidence": result.evidence}) # fmt: skip
68
+ out.line(f"{'OK' if result.ok else 'FAILED'} after {result.rounds} round(s): {result.message}")
69
+ out.detail(f"drafted by {drafted_by(cfg, provider)}", indent=2)
70
+ if result.notes:
71
+ out.line("Notes from the model:")
72
+ out.detail(result.notes, indent=2)
73
+ for f in result.files:
74
+ out.detail(f"wrote {f.relative_to(cfg.root)}", indent=2)
75
+ for e in result.evidence:
76
+ out.detail(f"evidence {e}", indent=2)
77
+ if not result.ok:
78
+ out.line("The last script is kept for you to finish by hand; it is not registered as a check.")
79
+ return GATE_FAILED
80
+ add_check(
81
+ cfg,
82
+ Check(
83
+ check_id,
84
+ "evidence",
85
+ result.covers,
86
+ {
87
+ "run": result.run,
88
+ "timeout": 900,
89
+ "require": list(args.require or []),
90
+ "drafted_by": drafted_by(cfg, provider),
91
+ },
92
+ ),
93
+ out,
94
+ )
95
+ skill = agents.install_verify_skill(cfg.root, cfg.name, check_id)
96
+ out.line(f"wrote {skill.relative_to(cfg.root)} so agents verify user-facing changes with `rung check {check_id}`.")
97
+ return OK
98
+
99
+
100
+ def _draft_property(args, cfg: Config, out: Output, sources: list[Path], rel: list[str]) -> int:
101
+ root = cfg.root
102
+ check_id = args.id or f"{_slug(rel)}-properties"
103
+ provider = _provider(cfg, args)
104
+ out.line(
105
+ f"Drafting property tests for {', '.join(rel)} with {drafted_by(cfg, provider)}; Rung runs them, then breaks "
106
+ "the code on purpose to see whether they notice ..."
107
+ )
108
+ result, target = property_draft.draft(
109
+ provider, root, sources, _slug(rel), intent=args.intent or "", out=args.out or ""
110
+ )
111
+ files = [f.relative_to(root).as_posix() for f in result.files]
112
+ out.emit({"ok": result.ok, "message": result.message, "rounds": result.rounds, "trace": result.trace,
113
+ "files": files, "warnings": result.warnings, "run": target.command}) # fmt: skip
114
+ out.line(f"{'OK' if result.ok else 'FAILED'} after {result.rounds} round(s): {result.message}")
115
+ out.detail(f"drafted by {drafted_by(cfg, provider)}", indent=2)
116
+ if result.notes:
117
+ out.line("Notes from the model:")
118
+ out.detail(result.notes, indent=2)
119
+ for f in files:
120
+ out.detail(f"wrote {f}", indent=2)
121
+ for w in result.warnings:
122
+ out.detail(w, indent=2)
123
+ if result.trace:
124
+ out.line("\nFailing example:")
125
+ out.detail(result.trace, indent=2)
126
+ out.line(
127
+ "\nIf this is a real bug, fix the code and run `rung check`. If the law is wrong, edit the test yourself."
128
+ )
129
+ return GATE_FAILED
130
+ if not result.ok:
131
+ return GATE_FAILED
132
+ # The verdict depends on the code under test, whatever it imports, and the runner's configuration:
133
+ # cover the whole project directory, and the test file itself.
134
+ covers = ["**/*"] if target.project == "." else [f"{target.project}/**"]
135
+ options = {"run": target.command, "drafted_by": drafted_by(cfg, provider), "specs": [target.test_file]}
136
+ if target.project != ".":
137
+ options["cwd"] = target.project
138
+ add_check(cfg, Check(check_id, "command", covers, options), out)
139
+ if added := laws.propose(root, result.laws, [check_id]):
140
+ out.line(f"Proposed {len(added)} law(s) in {laws.LAWS_FILE}: {', '.join(added)}.")
141
+ out.line("Read each one; approve with `rung laws --approve <id>` only if it says what you mean.")
142
+ return OK
143
+
144
+
145
+ def cmd_draft(args, out: Output) -> int:
146
+ cfg = load_config(args)
147
+ if args.kind == "runtime":
148
+ return _draft_runtime(args, cfg, out)
149
+ if not args.files:
150
+ raise ConfigError(f"`rung draft {args.kind}` needs the source files to model")
151
+ provider = _provider(cfg, args)
152
+ root = cfg.root
153
+ sources = [(root / p).resolve() for p in args.files]
154
+ missing = [str(s) for s in sources if not s.is_file()]
155
+ if missing:
156
+ raise ConfigError(f"not found: {', '.join(missing)}")
157
+ rel = [s.relative_to(root).as_posix() for s in sources]
158
+ if args.kind == "property":
159
+ return _draft_property(args, cfg, out, sources, rel)
160
+ check_id = args.id or f"{_slug(rel)}-{args.kind}"
161
+ target = root / (args.out or f"spec/{args.kind}/{_slug(rel)}")
162
+ target.mkdir(parents=True, exist_ok=True)
163
+ out.line(f"Drafting {args.kind} for {', '.join(rel)} with {drafted_by(cfg, provider)} ...")
164
+
165
+ drafter = tla_draft.draft if args.kind == "tla" else lean_draft.draft
166
+ result = drafter(provider, root, sources, target, intent=args.intent or "")
167
+ out.emit({"ok": result.ok, "message": result.message, "rounds": result.rounds, "trace": result.trace,
168
+ "files": [str(f.relative_to(root)) for f in result.files]}) # fmt: skip
169
+ out.line(f"{'OK' if result.ok else 'FAILED'} after {result.rounds} round(s): {result.message}")
170
+ out.detail(f"drafted by {drafted_by(cfg, provider)}", indent=2)
171
+ if result.notes:
172
+ out.line("Notes from the model:")
173
+ out.detail(result.notes, indent=2)
174
+ for f in result.files:
175
+ out.detail(f"wrote {f.relative_to(root)}", indent=2)
176
+ if result.trace:
177
+ out.line("\nCounterexample:")
178
+ out.detail(result.trace, indent=2)
179
+ out.line("\nIf this is a real bug, fix the code and run `rung check`. If the spec is wrong, edit it yourself.")
180
+ if not result.ok:
181
+ return GATE_FAILED
182
+
183
+ if args.kind == "tla":
184
+ spec = next(f for f in result.files if f.suffix == ".tla")
185
+ cfg_file = next(f for f in result.files if f.suffix == ".cfg")
186
+ options = {"spec": spec.relative_to(root).as_posix(), "config": cfg_file.relative_to(root).as_posix(),
187
+ "drafted_by": drafted_by(cfg, provider)} # fmt: skip
188
+ add_check(cfg, Check(check_id, "tla", rel, options), out)
189
+ else:
190
+ project = target.relative_to(root).as_posix()
191
+ add_check(
192
+ cfg,
193
+ Check(
194
+ check_id,
195
+ "lean",
196
+ [],
197
+ {"project": project, "theorems": result.theorems, "drafted_by": drafted_by(cfg, provider)},
198
+ ),
199
+ out,
200
+ )
201
+ exe = _lean_exe(target)
202
+ if exe and result.alphabet and (target / "drt_adapter.py").exists():
203
+ options = {
204
+ "model": f"lake exe {exe}", "model_cwd": project,
205
+ "impl": f"python3 {project}/drt_adapter.py", "alphabet": result.alphabet,
206
+ "cases": 2000, "max_len": 8, "specs": [f"{project}/**/*.lean", f"{project}/drt_adapter.py"],
207
+ } # fmt: skip
208
+ add_check(cfg, Check(f"{check_id}-drt", "drt", rel, options), out)
209
+ enforcing = [check_id] if args.kind == "tla" else [check_id, f"{check_id}-drt"]
210
+ added = laws.propose(root, result.laws, enforcing)
211
+ if added:
212
+ out.line(f"Proposed {len(added)} law(s) in {laws.LAWS_FILE}: {', '.join(added)}.")
213
+ out.line("Read each one; approve with `rung laws --approve <id>` only if it says what you mean.")
214
+ return OK
@@ -0,0 +1,112 @@
1
+ """Commands that change what is checked: laws, add, baseline, waive."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import date, timedelta
7
+ from pathlib import Path
8
+
9
+ from rung.checks import findings, registry, runner
10
+ from rung.checks.presets import PRESETS
11
+ from rung.checks.scan import ScanChecker
12
+ from rung.cli.commands.common import OK, add_check, load_config
13
+ from rung.cli.output import STATE_STYLE, VERDICT_STYLE, Output
14
+ from rung.core import git, laws
15
+ from rung.core.config import Check, ConfigError
16
+
17
+
18
+ def cmd_laws(args, out: Output) -> int:
19
+ cfg = load_config(args)
20
+ if args.approve:
21
+ try:
22
+ laws.set_status(cfg.root, args.approve, "approved")
23
+ except KeyError as exc:
24
+ raise ConfigError(f"no law {args.approve!r} in {laws.LAWS_FILE}") from exc
25
+ out.line(f"Approved {args.approve}.")
26
+ statuses = {s.check_id: s for s in runner.statuses(cfg)}
27
+ rows = [(law, *laws.state(law, statuses)) for law in laws.load(cfg.root)]
28
+ out.emit([{"id": law.id, "text": law.text, "state": st, "why": why, "enforced_by": law.enforced_by}
29
+ for law, st, why in rows]) # fmt: skip
30
+ if not rows:
31
+ out.line(f"No laws in {laws.LAWS_FILE} yet.")
32
+ for law, st, why in rows:
33
+ out.line(f"{out.badge(st, STATE_STYLE, 11)}{law.id}")
34
+ out.detail(law.text, indent=11)
35
+ out.detail(out.style(why, "dim"), indent=11)
36
+ return OK
37
+
38
+
39
+ def cmd_add(args, out: Output) -> int:
40
+ cfg = load_config(args)
41
+ options: dict[str, object] = {}
42
+ if args.kind in ("command", "evidence"):
43
+ if not args.run and args.kind == "command":
44
+ raise ConfigError("`rung add command` needs --run")
45
+ if args.run:
46
+ options["run"] = args.run
47
+ if args.cwd:
48
+ options["cwd"] = args.cwd
49
+ if args.require:
50
+ options["require"] = args.require
51
+ elif args.kind == "scan":
52
+ if not (args.tool or args.run):
53
+ raise ConfigError(f"`rung add scan` needs --tool (one of {', '.join(PRESETS)}) or --run")
54
+ if args.tool:
55
+ options["tool"] = args.tool
56
+ if args.run:
57
+ options["run"] = args.run
58
+ if args.cwd:
59
+ options["cwd"] = args.cwd
60
+ if args.fail_on:
61
+ options["fail_on"] = args.fail_on
62
+ elif args.kind == "deps":
63
+ options["min_age_days"] = args.min_age_days
64
+ add_check(cfg, Check(args.id, args.kind, args.covers or [], options), out)
65
+ return OK
66
+
67
+
68
+ def cmd_baseline(args, out: Output) -> int:
69
+ cfg = load_config(args)
70
+ check = cfg.check(args.check)
71
+ if check.kind not in ("scan", "shortcuts"):
72
+ raise ConfigError("baselines apply to `scan` and `shortcuts` checks")
73
+ checker = registry.get(check.kind)
74
+ assert isinstance(checker, ScanChecker)
75
+ found = checker.collect(cfg.root, check, check.get_int("timeout", 900))
76
+ path = findings.save_baseline(cfg.root, check.id, found, git.head(cfg.root))
77
+ out.emit({"check": check.id, "findings": len(found), "path": str(path.relative_to(cfg.root))})
78
+ out.line(f"Recorded {len(found)} existing finding(s) for {check.id!r} as accepted ({path.relative_to(cfg.root)}).")
79
+ out.line("Commit this file; from now on only findings beyond it will block.")
80
+ return OK
81
+
82
+
83
+ def cmd_waive(args, out: Output) -> int:
84
+ cfg = load_config(args)
85
+ check = cfg.check(args.check)
86
+ if not re.fullmatch(r"[0-9a-f]{16}", args.fingerprint):
87
+ raise ConfigError("a fingerprint is the 16-character hex id shown in brackets by `rung check`")
88
+ try:
89
+ expires = date.fromisoformat(args.until) if args.until else date.today() + timedelta(days=args.days)
90
+ except ValueError as exc:
91
+ raise ConfigError(f"--until must be a date like 2026-12-31, not {args.until!r}") from exc
92
+ if expires < date.today():
93
+ raise ConfigError("a waiver must expire in the future")
94
+ approver = args.approved_by or git.user_name(cfg.root)
95
+ if not approver:
96
+ raise ConfigError("cannot tell who approves this waiver; pass --approved-by")
97
+ w = findings.Waiver(check.id, args.fingerprint, args.reason, approver, expires)
98
+ path = findings.add_waiver(cfg.root, w)
99
+ out.emit({"check": w.check, "fingerprint": w.fingerprint, "expires": str(w.expires), "approved_by": approver})
100
+ out.line(f"Waived {w.fingerprint} on {w.check!r} until {w.expires} ({path.relative_to(cfg.root)}).")
101
+ return OK
102
+
103
+
104
+ def cmd_attest(args, out: Output) -> int:
105
+ cfg = load_config(args)
106
+ result = runner.attest(cfg, cfg.check(args.check), [Path(f) for f in args.files], by=args.by, note=args.note or "")
107
+ out.emit({"check": result.check.id, "verdict": result.verdict, "summary": result.outcome.summary,
108
+ "evidence": list(result.outcome.evidence)}) # fmt: skip
109
+ out.line(f"{out.badge('pass', VERDICT_STYLE, 10)} {result.check.id}: {result.outcome.summary}")
110
+ for line in result.outcome.lines:
111
+ out.detail(line, indent=11)
112
+ return OK
@@ -0,0 +1,114 @@
1
+ """Commands that run and report checks: check, status, report, prove, ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from rung.analysis import report
8
+ from rung.checks import prove, runner
9
+ from rung.cli.commands.common import GATE_FAILED, OK, load_config, resolve_base
10
+ from rung.cli.output import STATE_STYLE, VERDICT_STYLE, Output
11
+ from rung.core import git, ledger
12
+ from rung.core.config import CONFIG_NAME, find_root
13
+
14
+
15
+ def _print_result(result: runner.Result, out: Output) -> None:
16
+ o = result.outcome
17
+ out.line(f"{out.badge(o.verdict, VERDICT_STYLE, 6)}{result.check.id} ({result.check.kind}): {o.summary}")
18
+ for line in o.lines:
19
+ out.detail(line)
20
+
21
+
22
+ def cmd_check(args, out: Output) -> int:
23
+ cfg = load_config(args)
24
+ base: str | None = resolve_base(cfg.root, args.base) if args.changed or args.base else None
25
+ changed = git.changed_files(cfg.root, base) if args.changed and base else None
26
+ selected = runner.select(cfg, args.ids, changed)
27
+ if args.ci:
28
+ # Reuse a verdict only if it is fresh and was made against the same comparison base.
29
+ view = ledger.View(cfg.root)
30
+ selected = [c for c in selected if not runner.status(cfg, c, base=base or "", view=view).trusted]
31
+ if not cfg.checks:
32
+ out.error(f"no checks declared in {CONFIG_NAME}; see `rung plan` or `rung add`")
33
+ return GATE_FAILED
34
+ results = [runner.run_check(cfg, c, base=base) for c in selected]
35
+ out.emit({
36
+ "base": base,
37
+ "results": [{"id": r.check.id, "kind": r.check.kind, "verdict": r.verdict, "summary": r.outcome.summary,
38
+ "lines": r.outcome.lines, "findings": r.outcome.findings} for r in results],
39
+ }) # fmt: skip
40
+ if not results:
41
+ out.line("Nothing to run: every selected check is fresh or untouched by this change.")
42
+ for r in results:
43
+ _print_result(r, out)
44
+ return GATE_FAILED if any(r.verdict != "pass" for r in results) else OK
45
+
46
+
47
+ def cmd_status(args, out: Output) -> int:
48
+ cfg = load_config(args)
49
+ rows = runner.statuses(cfg)
50
+ problems = ledger.problems(cfg.root)
51
+ out.emit({
52
+ "checks": [{"id": s.check_id, "state": s.state, "at": (s.entry or {}).get("at"), "changed": s.changed}
53
+ for s in rows],
54
+ "ledger_problems": problems,
55
+ }) # fmt: skip
56
+ for s in rows:
57
+ when = (s.entry or {}).get("at", "")
58
+ out.line(f"{out.badge(s.state, STATE_STYLE, 10)} {s.check_id:28} {out.style(when, 'dim')}")
59
+ for path in s.changed:
60
+ out.detail(f"changed: {path}", indent=11)
61
+ for p in problems:
62
+ out.line(out.style(f"CORRUPT {p}", "red"))
63
+ if args.ci:
64
+ return OK if rows and all(s.trusted for s in rows) and not problems else GATE_FAILED
65
+ return OK
66
+
67
+
68
+ def cmd_report(args, out: Output) -> int:
69
+ cfg = load_config(args)
70
+ data = report.build(cfg)
71
+ if out.json_mode or args.format == "json":
72
+ out.json_mode = True
73
+ out.emit(data)
74
+ else:
75
+ print(report.markdown(data))
76
+ return OK
77
+
78
+
79
+ def cmd_prove(args, out: Output) -> int:
80
+ root = find_root(Path(args.dir))
81
+ base = args.base or git.default_base(root)
82
+ result = prove.prove(root, args.test_command, base, tests=args.tests or None, expect=args.expect, cwd=args.cwd)
83
+ out.emit({
84
+ "verdict": result.verdict, "summary": result.summary, "tests": result.tests,
85
+ "base": result.base.__dict__ if result.base else None, "head": result.head.__dict__ if result.head else None,
86
+ }) # fmt: skip
87
+ out.line(f"{out.badge(result.verdict, VERDICT_STYLE, 6)}{result.summary}")
88
+ for t in result.tests:
89
+ out.detail(f"test: {t}")
90
+ if result.base and result.verdict != "pass":
91
+ out.detail(f"merge base run (exit {result.base.returncode}):")
92
+ for line in result.base.tail[-12:]:
93
+ out.detail(line, indent=8)
94
+ return OK if result.verdict == "pass" else GATE_FAILED
95
+
96
+
97
+ def cmd_ledger(args, out: Output) -> int:
98
+ """Check every verdict file; with --prune, remove verdicts for checks no longer declared."""
99
+ cfg = load_config(args)
100
+ verdicts = ledger.read_all(cfg.root)
101
+ problems = ledger.problems(cfg.root)
102
+ declared = {c.id for c in cfg.checks}
103
+ orphans = [k for k, v in verdicts.items() if k not in declared and v.get("kind") != "proof" and "invalid" not in v]
104
+ if args.prune:
105
+ for check_id in orphans:
106
+ ledger.verdict_path(cfg.root, check_id).unlink(missing_ok=True)
107
+ out.emit({"verdicts": len(verdicts), "problems": problems, "orphans": orphans, "pruned": bool(args.prune)})
108
+ out.line(f"{len(verdicts)} verdict file(s); {len(problems)} corrupt")
109
+ for p in problems:
110
+ out.detail(p, indent=2)
111
+ if orphans:
112
+ verb = "removed" if args.prune else "for checks no longer in rung.toml (remove with --prune)"
113
+ out.line(f"{len(orphans)} verdict(s) {verb}: {', '.join(orphans)}")
114
+ return GATE_FAILED if problems else OK