rung-cli 0.1.2__tar.gz → 0.1.4__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.
- {rung_cli-0.1.2 → rung_cli-0.1.4}/.claude-plugin/plugin.json +1 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/CHANGELOG.md +48 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/PKG-INFO +41 -6
- {rung_cli-0.1.2 → rung_cli-0.1.4}/README.md +40 -5
- {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/CONFIG.md +6 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/DESIGN.md +6 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/pyproject.toml +1 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/skills/setup/SKILL.md +1 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/__init__.py +1 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/presets.py +3 -1
- rung_cli-0.1.4/src/rung/checks/prove.py +271 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/draft.py +19 -5
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/run.py +4 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/git.py +10 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/common.py +39 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/lean.py +15 -4
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/property.py +16 -7
- rung_cli-0.1.4/src/rung/drafting/routes.py +196 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/runtime.py +10 -3
- rung_cli-0.1.4/src/rung/drafting/sql_context.py +170 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/tla.py +27 -5
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/lean.md +3 -1
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/property.md +2 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/runtime.md +7 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/tla.md +4 -0
- rung_cli-0.1.4/tests/test_drafting_context.py +292 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_property.py +4 -0
- rung_cli-0.1.4/tests/test_prove.py +157 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_scan_checker.py +28 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/uv.lock +1 -1
- rung_cli-0.1.2/src/rung/checks/prove.py +0 -125
- rung_cli-0.1.2/tests/test_prove.py +0 -85
- {rung_cli-0.1.2 → rung_cli-0.1.4}/.claude-plugin/marketplace.json +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/.gitignore +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/CODE_OF_CONDUCT.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/CONTRIBUTING.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/LICENSE +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/SECURITY.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/RECIPES.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/RESEARCH.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/ROADMAP.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/LAWS.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/README.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/drt.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/queue.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/rung.toml +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/LeaseModel.lean +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/Main.lean +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lake-manifest.json +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lakefile.toml +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lean-toolchain +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/tla/LeaseQueue.cfg +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/tla/LeaseQueue.tla +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/__main__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/hooks.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/install.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/plan.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/report.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/survey.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/base.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/command.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/deps.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/findings.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/formal.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/registry.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/runner.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/scan.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/shortcuts.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/common.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/manage.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/setup.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/output.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/config.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/errors.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/laws.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/ledger.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/proc.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/redact.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/providers.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/sandbox.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/drt.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/lean.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/mutate.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/sarif.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/shortcuts.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/tla.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/rules.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/verify.md +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/__init__.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/conftest.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/gitleaks.sarif +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/osv.sarif +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/zizmor.sarif +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_agents_and_report.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_cli.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_config.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_deps.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_draft.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_drt.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_example_integration.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_gitutil.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_laws.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_lean.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_ledger.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_release_consistency.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_review_regressions.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_sarif_and_findings.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_shortcuts.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_survey_and_agents.py +0 -0
- {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_tla.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rung",
|
|
3
3
|
"description": "Set up Rung in a repository: record tests and security scans as checks, gate pull requests in CI, and teach coding agents the verification rules.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"author": { "name": "CalQuity" },
|
|
6
6
|
"homepage": "https://github.com/CalQuity/rung",
|
|
7
7
|
"repository": "https://github.com/CalQuity/rung",
|
|
@@ -3,7 +3,54 @@
|
|
|
3
3
|
All notable changes to Rung. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
and versions follow the policy in [CONTRIBUTING.md](CONTRIBUTING.md#versioning).
|
|
5
5
|
|
|
6
|
-
## 0.1.
|
|
6
|
+
## 0.1.4 (2026-11-01)
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- The `osv-scanner` preset runs `osv-scanner scan source --no-resolve`. Without it, a manifest naming
|
|
10
|
+
a package the registry does not have (`stripe-checkout-retry-pro` in `requirements.txt`) failed
|
|
11
|
+
transitive resolution, and the whole vulnerable-dependencies check was an ERROR. Manifests and
|
|
12
|
+
lockfiles are now scanned as written; the `deps` check reports the missing package. A check that
|
|
13
|
+
sets its own `run` is unaffected.
|
|
14
|
+
- `rung prove` with a pytest command judges each test the branch adds or changes, from a JUnit XML
|
|
15
|
+
report of both runs. A new test that passes (or skips) on the merge base fails the proof, by name,
|
|
16
|
+
even when another test in the same file fails there. `--json` and the proof's ledger entry gain
|
|
17
|
+
`cases` (each test with its outcome on the merge base and the branch). Other runners keep the
|
|
18
|
+
file-level rule.
|
|
19
|
+
- `rung draft tla` and `rung draft lean` cover the SQL files they added to the prompt (see below), so
|
|
20
|
+
a changed definition stales the verdict. `--json` gains `context` and `warnings`.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `rung draft tla` and `rung draft lean` find the database functions the sources call (`.rpc("name"`,
|
|
24
|
+
`self._rpc("name"`, and `select name(` / `call name(` in SQL strings), search the repository's
|
|
25
|
+
`*.sql` files for their `create [or replace] function` statements, and add every defining file to
|
|
26
|
+
the prompt, oldest commit first, telling the model the newest definition is the one that runs. The
|
|
27
|
+
command prints which files it added. A call whose definition cannot be found is a warning, on the
|
|
28
|
+
terminal and in the result, because the model then has to assume the call is atomic.
|
|
29
|
+
- `rung draft runtime` shows the model the routes the app serves, with `file:line`: FastAPI,
|
|
30
|
+
Starlette and Flask decorators (with an `APIRouter(prefix=...)` or `Blueprint(url_prefix=...)` in
|
|
31
|
+
the same file), Express, Hono and Koa handlers, router mounts, and the Next.js app and pages
|
|
32
|
+
routers. The model is told to drive only listed routes and to say in its notes when the intent
|
|
33
|
+
names a feature that does not exist.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- A drafted spec, Lean adapter, drive script or property test that hard-codes a home or temporary
|
|
37
|
+
directory path (`/Users/<name>/`, `/home/<name>/`, `C:\Users\`, `/tmp`, `/var/folders/`) is sent
|
|
38
|
+
back to the model as a repair error, before Rung runs it. Before, a Lean adapter could fall back to
|
|
39
|
+
the drafting developer's own checkout and fail on everyone else's machine.
|
|
40
|
+
|
|
41
|
+
## 0.1.3 (2026-11-01)
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- `rung draft property`: a plain `assert` the model wrote that fails on the real code is now a
|
|
45
|
+
finding for a person, not a broken test. Before, a failure without Hypothesis's "Falsifying
|
|
46
|
+
example" marker was sent back to the model to rewrite, up to four times, which could hide the bug
|
|
47
|
+
the tests had found. A mutant that stops the module compiling is still not counted as caught.
|
|
48
|
+
|
|
49
|
+
### Documentation
|
|
50
|
+
- The README describes the runtime verification kit: the drive script, the `FEATURES.md` feature
|
|
51
|
+
map, the project verification skill for coding agents, and the recordings each run leaves.
|
|
52
|
+
|
|
53
|
+
## 0.1.2 (2026-11-01)
|
|
7
54
|
|
|
8
55
|
The first release on PyPI, as `rung-cli`.
|
|
9
56
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rung-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: An opinionated verification ladder for software written with coding agents: architecture rules, runtime evidence, properties, TLA+ and Lean, and an evidence ledger that expires when code changes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/CalQuity/rung
|
|
6
6
|
Project-URL: Documentation, https://github.com/CalQuity/rung/tree/main/docs
|
|
@@ -122,6 +122,33 @@ A refund never exceeds the original charge, even under concurrent retries.
|
|
|
122
122
|
`rung laws` shows whether each law currently **holds**: approved, enforced, and every enforcing
|
|
123
123
|
verdict fresh. Agents may propose laws; only people approve them.
|
|
124
124
|
|
|
125
|
+
## A verification kit for your running app
|
|
126
|
+
|
|
127
|
+
`rung draft runtime --intent "apply a coupon at checkout and see the total update"` gives your
|
|
128
|
+
project a verification kit, written by the model and registered as a check only once it has worked
|
|
129
|
+
against the real app:
|
|
130
|
+
|
|
131
|
+
- **`.rung/verify/drive.py`**, a script that starts the app on a free port, drives a feature the way
|
|
132
|
+
a user does (Playwright for web UIs, HTTP or CLI calls otherwise) and asserts on what a user would
|
|
133
|
+
see. It never calls internal functions or test-only endpoints. The model is shown the routes the
|
|
134
|
+
app really serves, found in its code (FastAPI, Starlette and Flask decorators, Express, Hono and
|
|
135
|
+
Koa handlers, Next.js app and pages routers), and drives only those; when the intent names a
|
|
136
|
+
feature the app does not have, it says so in its notes instead of guessing URLs.
|
|
137
|
+
- **`.rung/verify/FEATURES.md`**, a feature map: for each user-facing feature, how a user reaches it,
|
|
138
|
+
how the script drives it, what observable state proves it works, and the gotchas (seed data,
|
|
139
|
+
logins, flaky spots).
|
|
140
|
+
- **A project verification skill** (`.claude/skills/verify-<project>/`) that tells the project's
|
|
141
|
+
coding agents to drive the app after any user-facing change, extend the script and feature map
|
|
142
|
+
when they add a feature, and treat a failure as a product bug rather than weaken the script.
|
|
143
|
+
- **Recordings.** Every run writes its evidence to a fresh directory: a video of the browser session
|
|
144
|
+
(`--require .webm` demands one), screenshots, request and response logs, a transcript and a
|
|
145
|
+
structured `result.json`. Their hashes go into the verdict, which stays valid only while the code
|
|
146
|
+
it covers is unchanged.
|
|
147
|
+
|
|
148
|
+
Where a script cannot reach a feature (a login flow, a native app), an agent can drive it with its
|
|
149
|
+
own browser or computer-use tool and record it with `rung attest <check> <files> --by <tool>`. Such
|
|
150
|
+
evidence is recorded with who captured it and is never re-run, so it is weaker than a script.
|
|
151
|
+
|
|
125
152
|
## Property tests that are tested themselves
|
|
126
153
|
|
|
127
154
|
`rung draft property pricing.py --intent "a total is never negative and equals the discounted sum"`
|
|
@@ -147,11 +174,19 @@ needs them, and the model drafts the first version.
|
|
|
147
174
|
|
|
148
175
|
- **`rung draft tla queue.py`** writes a TLA+ model whose actions copy the code's real guards,
|
|
149
176
|
then runs TLC. A counterexample stops the draft and goes to you. The model never gets to
|
|
150
|
-
edit the spec until the checker is quiet.
|
|
177
|
+
edit the spec until the checker is quiet. When the code calls database functions
|
|
178
|
+
(`supabase.rpc("claim_job")`, `select claim_job(...)` in SQL strings), Rung adds the `*.sql`
|
|
179
|
+
files that define them to the prompt, oldest definition first, and covers them with the check;
|
|
180
|
+
a call whose definition it cannot find is a warning, because the model then has to assume the
|
|
181
|
+
call is atomic.
|
|
151
182
|
- **`rung draft lean pricing.py`** writes a small Lean model of the intended behaviour, proves
|
|
152
183
|
its laws, and sets up a differential test that holds the real code to it on random inputs.
|
|
153
184
|
Lean checks pass only when `#print axioms` shows no `sorry`, no `native_decide` and no
|
|
154
|
-
invented axioms.
|
|
185
|
+
invented axioms. It is shown the same SQL definitions.
|
|
186
|
+
|
|
187
|
+
Every drafted file (spec, adapter, drive script, property test) is sent back to the model if it
|
|
188
|
+
hard-codes a home or temporary directory path (`/Users/<name>/`, `/home/<name>/`, `C:\Users\`,
|
|
189
|
+
`/tmp`), because it would run on one machine only.
|
|
155
190
|
|
|
156
191
|
[`examples/lease-queue`](https://github.com/CalQuity/rung/tree/main/examples/lease-queue) shows both catching a real bug, plus a
|
|
157
192
|
three-step case only the differential test finds.
|
|
@@ -171,7 +206,7 @@ jobs:
|
|
|
171
206
|
with:
|
|
172
207
|
fetch-depth: 0
|
|
173
208
|
persist-credentials: false
|
|
174
|
-
- uses: CalQuity/rung@v0.1.
|
|
209
|
+
- uses: CalQuity/rung@v0.1.4
|
|
175
210
|
```
|
|
176
211
|
|
|
177
212
|
The action runs the checks a pull request touches against its merge base. It keeps a single
|
|
@@ -186,7 +221,7 @@ To run the same check before every push, add Rung to `.pre-commit-config.yaml`:
|
|
|
186
221
|
|
|
187
222
|
```yaml
|
|
188
223
|
- repo: https://github.com/CalQuity/rung
|
|
189
|
-
rev: v0.1.
|
|
224
|
+
rev: v0.1.4
|
|
190
225
|
hooks:
|
|
191
226
|
- id: rung-check
|
|
192
227
|
```
|
|
@@ -213,7 +248,7 @@ any OpenAI-compatible endpoint, including local models. Rung never stores creden
|
|
|
213
248
|
| `rung check [ids] [--changed] [--base REF]` | Run checks and record verdicts |
|
|
214
249
|
| `rung status [--ci]` | Fresh, stale, failing; `--ci` exits 1 unless all fresh |
|
|
215
250
|
| `rung report [--format md\|json]` | Checks, laws, new findings and fix proofs |
|
|
216
|
-
| `rung prove 'pytest tests/test_x.py'` | Show a new test fails without the fix and passes with it |
|
|
251
|
+
| `rung prove 'pytest tests/test_x.py'` | Show a new test fails without the fix and passes with it; under pytest, each new or changed test on its own |
|
|
217
252
|
| `rung laws [--approve ID]` | Which laws hold |
|
|
218
253
|
| `rung add <kind> <id> ...` | Declare a `command`, `evidence`, `scan` or `deps` check |
|
|
219
254
|
| `rung attest <check> <files> --by <tool>` | Record evidence an agent or person captured (Claude in Chrome, Codex, computer use) |
|
|
@@ -104,6 +104,33 @@ A refund never exceeds the original charge, even under concurrent retries.
|
|
|
104
104
|
`rung laws` shows whether each law currently **holds**: approved, enforced, and every enforcing
|
|
105
105
|
verdict fresh. Agents may propose laws; only people approve them.
|
|
106
106
|
|
|
107
|
+
## A verification kit for your running app
|
|
108
|
+
|
|
109
|
+
`rung draft runtime --intent "apply a coupon at checkout and see the total update"` gives your
|
|
110
|
+
project a verification kit, written by the model and registered as a check only once it has worked
|
|
111
|
+
against the real app:
|
|
112
|
+
|
|
113
|
+
- **`.rung/verify/drive.py`**, a script that starts the app on a free port, drives a feature the way
|
|
114
|
+
a user does (Playwright for web UIs, HTTP or CLI calls otherwise) and asserts on what a user would
|
|
115
|
+
see. It never calls internal functions or test-only endpoints. The model is shown the routes the
|
|
116
|
+
app really serves, found in its code (FastAPI, Starlette and Flask decorators, Express, Hono and
|
|
117
|
+
Koa handlers, Next.js app and pages routers), and drives only those; when the intent names a
|
|
118
|
+
feature the app does not have, it says so in its notes instead of guessing URLs.
|
|
119
|
+
- **`.rung/verify/FEATURES.md`**, a feature map: for each user-facing feature, how a user reaches it,
|
|
120
|
+
how the script drives it, what observable state proves it works, and the gotchas (seed data,
|
|
121
|
+
logins, flaky spots).
|
|
122
|
+
- **A project verification skill** (`.claude/skills/verify-<project>/`) that tells the project's
|
|
123
|
+
coding agents to drive the app after any user-facing change, extend the script and feature map
|
|
124
|
+
when they add a feature, and treat a failure as a product bug rather than weaken the script.
|
|
125
|
+
- **Recordings.** Every run writes its evidence to a fresh directory: a video of the browser session
|
|
126
|
+
(`--require .webm` demands one), screenshots, request and response logs, a transcript and a
|
|
127
|
+
structured `result.json`. Their hashes go into the verdict, which stays valid only while the code
|
|
128
|
+
it covers is unchanged.
|
|
129
|
+
|
|
130
|
+
Where a script cannot reach a feature (a login flow, a native app), an agent can drive it with its
|
|
131
|
+
own browser or computer-use tool and record it with `rung attest <check> <files> --by <tool>`. Such
|
|
132
|
+
evidence is recorded with who captured it and is never re-run, so it is weaker than a script.
|
|
133
|
+
|
|
107
134
|
## Property tests that are tested themselves
|
|
108
135
|
|
|
109
136
|
`rung draft property pricing.py --intent "a total is never negative and equals the discounted sum"`
|
|
@@ -129,11 +156,19 @@ needs them, and the model drafts the first version.
|
|
|
129
156
|
|
|
130
157
|
- **`rung draft tla queue.py`** writes a TLA+ model whose actions copy the code's real guards,
|
|
131
158
|
then runs TLC. A counterexample stops the draft and goes to you. The model never gets to
|
|
132
|
-
edit the spec until the checker is quiet.
|
|
159
|
+
edit the spec until the checker is quiet. When the code calls database functions
|
|
160
|
+
(`supabase.rpc("claim_job")`, `select claim_job(...)` in SQL strings), Rung adds the `*.sql`
|
|
161
|
+
files that define them to the prompt, oldest definition first, and covers them with the check;
|
|
162
|
+
a call whose definition it cannot find is a warning, because the model then has to assume the
|
|
163
|
+
call is atomic.
|
|
133
164
|
- **`rung draft lean pricing.py`** writes a small Lean model of the intended behaviour, proves
|
|
134
165
|
its laws, and sets up a differential test that holds the real code to it on random inputs.
|
|
135
166
|
Lean checks pass only when `#print axioms` shows no `sorry`, no `native_decide` and no
|
|
136
|
-
invented axioms.
|
|
167
|
+
invented axioms. It is shown the same SQL definitions.
|
|
168
|
+
|
|
169
|
+
Every drafted file (spec, adapter, drive script, property test) is sent back to the model if it
|
|
170
|
+
hard-codes a home or temporary directory path (`/Users/<name>/`, `/home/<name>/`, `C:\Users\`,
|
|
171
|
+
`/tmp`), because it would run on one machine only.
|
|
137
172
|
|
|
138
173
|
[`examples/lease-queue`](https://github.com/CalQuity/rung/tree/main/examples/lease-queue) shows both catching a real bug, plus a
|
|
139
174
|
three-step case only the differential test finds.
|
|
@@ -153,7 +188,7 @@ jobs:
|
|
|
153
188
|
with:
|
|
154
189
|
fetch-depth: 0
|
|
155
190
|
persist-credentials: false
|
|
156
|
-
- uses: CalQuity/rung@v0.1.
|
|
191
|
+
- uses: CalQuity/rung@v0.1.4
|
|
157
192
|
```
|
|
158
193
|
|
|
159
194
|
The action runs the checks a pull request touches against its merge base. It keeps a single
|
|
@@ -168,7 +203,7 @@ To run the same check before every push, add Rung to `.pre-commit-config.yaml`:
|
|
|
168
203
|
|
|
169
204
|
```yaml
|
|
170
205
|
- repo: https://github.com/CalQuity/rung
|
|
171
|
-
rev: v0.1.
|
|
206
|
+
rev: v0.1.4
|
|
172
207
|
hooks:
|
|
173
208
|
- id: rung-check
|
|
174
209
|
```
|
|
@@ -195,7 +230,7 @@ any OpenAI-compatible endpoint, including local models. Rung never stores creden
|
|
|
195
230
|
| `rung check [ids] [--changed] [--base REF]` | Run checks and record verdicts |
|
|
196
231
|
| `rung status [--ci]` | Fresh, stale, failing; `--ci` exits 1 unless all fresh |
|
|
197
232
|
| `rung report [--format md\|json]` | Checks, laws, new findings and fix proofs |
|
|
198
|
-
| `rung prove 'pytest tests/test_x.py'` | Show a new test fails without the fix and passes with it |
|
|
233
|
+
| `rung prove 'pytest tests/test_x.py'` | Show a new test fails without the fix and passes with it; under pytest, each new or changed test on its own |
|
|
199
234
|
| `rung laws [--approve ID]` | Which laws hold |
|
|
200
235
|
| `rung add <kind> <id> ...` | Declare a `command`, `evidence`, `scan` or `deps` check |
|
|
201
236
|
| `rung attest <check> <files> --by <tool>` | Record evidence an agent or person captured (Claude in Chrome, Codex, computer use) |
|
|
@@ -90,6 +90,12 @@ can reach the feature.
|
|
|
90
90
|
| `default_level` | preset's | Level for results the tool reports without one |
|
|
91
91
|
| `covers` | preset's | gitleaks/semgrep: everything; osv-scanner: manifests and lockfiles; zizmor: workflows |
|
|
92
92
|
|
|
93
|
+
The `osv-scanner` preset runs `osv-scanner scan source --no-resolve`: manifests and lockfiles are
|
|
94
|
+
scanned as written, without resolving transitive dependencies, because one package the registry does
|
|
95
|
+
not have (an invented or private name in `requirements.txt`) makes resolution fail the whole scan.
|
|
96
|
+
The `deps` check is what reports such a package. A project that wants transitive resolution can set
|
|
97
|
+
`run` to the command without `--no-resolve`.
|
|
98
|
+
|
|
93
99
|
On `--changed` or `--base`, the scanner also runs on the merge base, in a temporary worktree,
|
|
94
100
|
and only new findings count. Otherwise findings are compared with `.rung/baselines/<id>.json`
|
|
95
101
|
if present. Without either, every finding counts as new.
|
|
@@ -74,6 +74,12 @@ A regression test that passes before the fix proves nothing, and agents write su
|
|
|
74
74
|
they must fail, then runs them on the branch where they must pass. `--expect` names text the
|
|
75
75
|
failure must contain, so an import error does not count as the bug being caught.
|
|
76
76
|
|
|
77
|
+
One failing test can hide a weak one in the same file, so when the command runs pytest, both runs
|
|
78
|
+
also write a JUnit XML report and each test the branch adds or changes (found by comparing the test
|
|
79
|
+
functions' code with the merge base) is judged on its own. Any of them that passes or skips on the
|
|
80
|
+
merge base fails the proof, by name. `--json` lists each one as `cases`, with its outcome on both
|
|
81
|
+
sides. Other runners are judged by the whole run.
|
|
82
|
+
|
|
77
83
|
### Runtime evidence that proves itself
|
|
78
84
|
|
|
79
85
|
`rung draft runtime` has the model write a drive script and a feature map. Rung runs the script
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rung-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
description = "An opinionated verification ladder for software written with coding agents: architecture rules, runtime evidence, properties, TLA+ and Lean, and an evidence ledger that expires when code changes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -43,7 +43,9 @@ PRESETS: dict[str, Preset] = {
|
|
|
43
43
|
Preset(
|
|
44
44
|
"osv-scanner",
|
|
45
45
|
"dependencies with known vulnerabilities",
|
|
46
|
-
|
|
46
|
+
# --no-resolve scans manifests as written. Transitive resolution fails the whole scan when a
|
|
47
|
+
# manifest names a package the registry does not have; the `deps` check reports that package.
|
|
48
|
+
"osv-scanner scan source --no-resolve -r . -f sarif --output-file {out}",
|
|
47
49
|
"brew install osv-scanner | https://github.com/google/osv-scanner",
|
|
48
50
|
max_age_days=1,
|
|
49
51
|
covers=[
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""`rung prove`: show that a new test actually catches the bug it claims to.
|
|
2
|
+
|
|
3
|
+
A regression test that passes before the fix proves nothing; agents write such
|
|
4
|
+
tests all the time. The proof is mechanical: take the test files changed on this
|
|
5
|
+
branch, put them on top of the merge base (the code *without* the fix), and run
|
|
6
|
+
the test command there. It must fail. Then run it on the branch. It must pass.
|
|
7
|
+
Both runs are recorded in the ledger.
|
|
8
|
+
|
|
9
|
+
When the base fails for the wrong reason, typically an import error because the
|
|
10
|
+
fix added a new function, `--expect` names text the base failure must contain,
|
|
11
|
+
such as the assertion or the test name.
|
|
12
|
+
|
|
13
|
+
A file can hold several new tests, and one failing on the base says nothing about the
|
|
14
|
+
others. When the command runs pytest, both runs also write a JUnit XML report, and each
|
|
15
|
+
test the branch adds or changes is judged on its own: it must fail on the merge base and
|
|
16
|
+
pass on the branch. A new test that passes on the base proves nothing about the fix, so
|
|
17
|
+
the proof fails and names it. Other runners are judged by the whole run, as before.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import itertools
|
|
24
|
+
import re
|
|
25
|
+
import shlex
|
|
26
|
+
import shutil
|
|
27
|
+
import tempfile
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from pathlib import Path, PurePosixPath
|
|
31
|
+
|
|
32
|
+
from rung.core import git, ledger
|
|
33
|
+
from rung.core.errors import RungError
|
|
34
|
+
from rung.core.proc import run as run_process
|
|
35
|
+
from rung.core.redact import redact_lines
|
|
36
|
+
|
|
37
|
+
TEST_PATTERNS = [
|
|
38
|
+
"**/test_*.py", "**/*_test.py", "**/tests/**", "**/__tests__/**", "**/*.test.*",
|
|
39
|
+
"**/*.spec.*", "**/*_test.go", "**/spec/**", "**/*Test.java", "**/*Tests.cs",
|
|
40
|
+
] # fmt: skip
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Run:
|
|
45
|
+
returncode: int
|
|
46
|
+
tail: list[str]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Case:
|
|
51
|
+
"""One test the branch adds or changes, and its outcome on each side."""
|
|
52
|
+
|
|
53
|
+
test: str # `tests/test_money.py::test_refund` or `...::TestRefund::test_cap`
|
|
54
|
+
base: str # passed | failed | skipped | missing (did not run)
|
|
55
|
+
head: str
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def proven(self) -> bool:
|
|
59
|
+
return self.base == "failed" and self.head == "passed"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Proof:
|
|
64
|
+
verdict: str # pass | fail | error
|
|
65
|
+
summary: str
|
|
66
|
+
tests: list[str] = field(default_factory=list)
|
|
67
|
+
base: Run | None = None
|
|
68
|
+
head: Run | None = None
|
|
69
|
+
cases: list[Case] = field(default_factory=list) # per test, when the runner reports them (pytest)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_test_file(path: str) -> bool:
|
|
73
|
+
return any(git.glob_match(p, path) for p in TEST_PATTERNS)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run(argv: list[str], cwd: Path, timeout: int) -> Run:
|
|
77
|
+
result = run_process(argv, cwd=cwd, timeout=timeout)
|
|
78
|
+
return Run(result.returncode, (result.stdout + result.stderr).strip().splitlines()[-25:])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def runs_pytest(argv: list[str]) -> bool:
|
|
82
|
+
"""Whether a command runs pytest: `pytest ...`, `uv run pytest ...`, `python -m pytest ...`."""
|
|
83
|
+
if any(PurePosixPath(a.replace("\\", "/")).name in ("pytest", "py.test", "pytest.exe") for a in argv):
|
|
84
|
+
return True
|
|
85
|
+
return any(a == "-m" and b == "pytest" for a, b in itertools.pairwise(argv))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def python_tests(source: str) -> dict[str, str]:
|
|
89
|
+
"""Each pytest test in `source` (`test_x`, `TestY.test_z`) -> a dump of its code, decorators included."""
|
|
90
|
+
try:
|
|
91
|
+
tree = ast.parse(source)
|
|
92
|
+
except SyntaxError:
|
|
93
|
+
return {}
|
|
94
|
+
tests = {}
|
|
95
|
+
for node in tree.body:
|
|
96
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name.startswith("test"):
|
|
97
|
+
tests[node.name] = ast.dump(node)
|
|
98
|
+
elif isinstance(node, ast.ClassDef) and node.name.startswith("Test"):
|
|
99
|
+
for item in node.body:
|
|
100
|
+
if isinstance(item, ast.FunctionDef | ast.AsyncFunctionDef) and item.name.startswith("test"):
|
|
101
|
+
tests[f"{node.name}.{item.name}"] = ast.dump(item)
|
|
102
|
+
return tests
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def new_or_changed_tests(root: Path, base: str, files: list[str]) -> dict[str, list[str]]:
|
|
106
|
+
"""file -> the tests in it that the branch adds or whose code it changes (Python test files only)."""
|
|
107
|
+
found = {}
|
|
108
|
+
for rel in files:
|
|
109
|
+
if not rel.endswith(".py"):
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
before = python_tests(git.show(root, base, rel))
|
|
113
|
+
except git.GitError:
|
|
114
|
+
before = {}
|
|
115
|
+
after = python_tests((root / rel).read_text(errors="ignore"))
|
|
116
|
+
if changed := [name for name, code in after.items() if before.get(name) != code]:
|
|
117
|
+
found[rel] = changed
|
|
118
|
+
return found
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def junit_outcomes(path: Path) -> dict[tuple[str, str], str]:
|
|
122
|
+
"""(classname, test name without parameters) -> passed | failed | skipped, from a JUnit XML report.
|
|
123
|
+
|
|
124
|
+
A parametrized test failed if any case failed; it passed if none failed and one passed.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
tree = ET.parse(path)
|
|
128
|
+
except (OSError, ET.ParseError):
|
|
129
|
+
return {}
|
|
130
|
+
outcomes: dict[tuple[str, str], str] = {}
|
|
131
|
+
rank = {"skipped": 0, "passed": 1, "failed": 2}
|
|
132
|
+
for case in tree.iter("testcase"):
|
|
133
|
+
key = (case.get("classname", ""), case.get("name", "").split("[", 1)[0])
|
|
134
|
+
tags = {child.tag for child in case}
|
|
135
|
+
outcome = "failed" if tags & {"failure", "error"} else "skipped" if "skipped" in tags else "passed"
|
|
136
|
+
if rank[outcome] >= rank.get(outcomes.get(key, "skipped"), 0):
|
|
137
|
+
outcomes[key] = outcome
|
|
138
|
+
return outcomes
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _qualname(classname: str, name: str, module: list[str]) -> str | None:
|
|
142
|
+
"""The test's name inside its module, if the JUnit classname belongs to the module at path `module`."""
|
|
143
|
+
for start in range(len(module)):
|
|
144
|
+
dotted = ".".join(module[start:])
|
|
145
|
+
if classname == dotted:
|
|
146
|
+
return name
|
|
147
|
+
if classname.startswith(dotted + "."):
|
|
148
|
+
return f"{classname[len(dotted) + 1 :]}.{name}"
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def per_test(
|
|
153
|
+
wanted: dict[str, list[str]], base: dict[tuple[str, str], str], head: dict[tuple[str, str], str]
|
|
154
|
+
) -> list[Case]:
|
|
155
|
+
"""Match the new or changed tests to the outcomes of both runs."""
|
|
156
|
+
|
|
157
|
+
def outcome(results: dict[tuple[str, str], str], rel: str, test: str) -> str:
|
|
158
|
+
module = list(PurePosixPath(rel).with_suffix("").parts)
|
|
159
|
+
hits = [o for (cls, name), o in results.items() if _qualname(cls, name, module) == test]
|
|
160
|
+
return max(hits, key=["skipped", "passed", "failed"].index) if hits else "missing"
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
Case(f"{rel}::{test.replace('.', '::')}", outcome(base, rel, test), outcome(head, rel, test))
|
|
164
|
+
for rel, tests in sorted(wanted.items())
|
|
165
|
+
for test in tests
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _names(cases: list[Case]) -> str:
|
|
170
|
+
return ", ".join(c.test for c in cases)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Output that means the tests never reached an assertion on the base: they could not even load.
|
|
174
|
+
SETUP_FAILURE = re.compile(
|
|
175
|
+
r"ModuleNotFoundError|ImportError|SyntaxError|NameError: name .* is not defined|error collecting|"
|
|
176
|
+
r"ERROR collecting|errors? during collection|no tests ran|collected 0 items|Cannot find module|"
|
|
177
|
+
r"is not exported|has no exported member|TS2307|TS2305|undefined: |cannot find package",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def prove(
|
|
182
|
+
root: Path, command: str, base_ref: str, *, tests: list[str] | None = None,
|
|
183
|
+
expect: str | None = None, timeout: int = 1800, cwd: str = ".",
|
|
184
|
+
) -> Proof: # fmt: skip
|
|
185
|
+
mb = git.merge_base(root, base_ref)
|
|
186
|
+
changed = git.changed_files(root, base_ref)
|
|
187
|
+
chosen = sorted(tests) if tests else sorted(f for f in changed if is_test_file(f))
|
|
188
|
+
if not chosen:
|
|
189
|
+
return Proof("error", "no changed test files on this branch; name them with --tests")
|
|
190
|
+
missing = [t for t in chosen if not (root / t).is_file()]
|
|
191
|
+
if missing:
|
|
192
|
+
raise RungError(f"test file(s) not found: {', '.join(missing)}")
|
|
193
|
+
sources = sorted(f for f in changed if not is_test_file(f) and (root / f).is_file())
|
|
194
|
+
before = ledger.snapshot(root, sources + chosen)
|
|
195
|
+
|
|
196
|
+
argv = shlex.split(command)
|
|
197
|
+
wanted = new_or_changed_tests(root, mb, chosen) if runs_pytest(argv) else {}
|
|
198
|
+
with tempfile.TemporaryDirectory(prefix="rung-prove-") as tmp:
|
|
199
|
+
report = {side: Path(tmp) / f"{side}.xml" for side in ("base", "head")}
|
|
200
|
+
extra = {side: [f"--junitxml={path}"] if wanted else [] for side, path in report.items()}
|
|
201
|
+
with git.worktree(root, mb) as tree:
|
|
202
|
+
for rel in chosen:
|
|
203
|
+
dest = tree / rel
|
|
204
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
shutil.copy2(root / rel, dest)
|
|
206
|
+
base_run = _run(argv + extra["base"], tree / cwd, timeout)
|
|
207
|
+
head_run = _run(argv + extra["head"], root / cwd, timeout)
|
|
208
|
+
cases = per_test(wanted, junit_outcomes(report["base"]), junit_outcomes(report["head"])) if wanted else []
|
|
209
|
+
if cases and all(c.head == "missing" for c in cases):
|
|
210
|
+
cases = [] # the report did not name them (an unusual rootdir or plugin): judge the whole run
|
|
211
|
+
|
|
212
|
+
proof = Proof("pass", "", chosen, base_run, head_run, cases)
|
|
213
|
+
base_text = "\n".join(base_run.tail)
|
|
214
|
+
weak = [c for c in cases if c.base in ("passed", "skipped")]
|
|
215
|
+
not_run = [c for c in cases if c.base == "missing"]
|
|
216
|
+
not_passing = [c for c in cases if c.head != "passed"]
|
|
217
|
+
if base_run.returncode == 0:
|
|
218
|
+
proof.verdict, proof.summary = "fail", "the tests pass without the fix, so they do not catch the bug"
|
|
219
|
+
elif weak:
|
|
220
|
+
proof.verdict = "fail"
|
|
221
|
+
proof.summary = (
|
|
222
|
+
f"{len(weak)} new or changed test(s) also pass (or skip) on the merge base, so they prove nothing "
|
|
223
|
+
f"about the fix: {_names(weak)}"
|
|
224
|
+
)
|
|
225
|
+
elif expect and expect not in base_text:
|
|
226
|
+
proof.verdict = "fail"
|
|
227
|
+
proof.summary = f"the base run failed, but not with {expect!r}; it may be failing for another reason"
|
|
228
|
+
elif not expect and SETUP_FAILURE.search(base_text):
|
|
229
|
+
proof.verdict = "fail"
|
|
230
|
+
proof.summary = (
|
|
231
|
+
"on the merge base the tests failed while loading (an import or collection error), not on an "
|
|
232
|
+
"assertion. If that failure is the bug, re-run with --expect '<text of the failing assertion>'"
|
|
233
|
+
)
|
|
234
|
+
elif not expect and not_run:
|
|
235
|
+
proof.verdict = "fail"
|
|
236
|
+
proof.summary = f"{len(not_run)} new or changed test(s) did not run on the merge base: {_names(not_run)}"
|
|
237
|
+
elif head_run.returncode != 0:
|
|
238
|
+
proof.verdict = "fail"
|
|
239
|
+
proof.summary = "the tests fail on this branch too" + (f": {_names(not_passing)}" if not_passing else "")
|
|
240
|
+
elif not_passing:
|
|
241
|
+
proof.verdict = "fail"
|
|
242
|
+
proof.summary = f"{len(not_passing)} new or changed test(s) did not pass on this branch: {_names(not_passing)}"
|
|
243
|
+
elif cases:
|
|
244
|
+
proof.summary = (
|
|
245
|
+
f"{len(cases)} new or changed test(s) fail on merge base {mb[:10]} and pass on this branch: "
|
|
246
|
+
+ _names(cases)
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
proof.summary = f"fails on merge base {mb[:10]}, passes on this branch"
|
|
250
|
+
if ledger.snapshot(root, sources + chosen) != before:
|
|
251
|
+
proof.verdict, proof.summary = "error", "files changed while the proof ran; re-run it"
|
|
252
|
+
|
|
253
|
+
slug = re.sub(r"[^a-z0-9]+", "-", command.lower()).strip("-")[:40] or "tests"
|
|
254
|
+
ledger.record(
|
|
255
|
+
root,
|
|
256
|
+
check_id=f"proof:{slug}",
|
|
257
|
+
kind="proof",
|
|
258
|
+
verdict=proof.verdict,
|
|
259
|
+
covers=before,
|
|
260
|
+
specs={},
|
|
261
|
+
summary=proof.summary,
|
|
262
|
+
context=mb,
|
|
263
|
+
details={
|
|
264
|
+
"command": command, "cwd": cwd, "base": mb, "tests": chosen, "expect": expect or "",
|
|
265
|
+
"files": sorted(before),
|
|
266
|
+
"base_returncode": base_run.returncode, "head_returncode": head_run.returncode,
|
|
267
|
+
"base_tail": redact_lines(base_run.tail), "head_tail": redact_lines(head_run.tail),
|
|
268
|
+
"cases": [c.__dict__ for c in cases],
|
|
269
|
+
},
|
|
270
|
+
) # fmt: skip
|
|
271
|
+
return proof
|
|
@@ -15,7 +15,7 @@ from rung.core import laws
|
|
|
15
15
|
from rung.core.config import Check, Config, ConfigError
|
|
16
16
|
from rung.drafting import lean as lean_draft
|
|
17
17
|
from rung.drafting import property as property_draft
|
|
18
|
-
from rung.drafting import providers, runtime
|
|
18
|
+
from rung.drafting import providers, runtime, sql_context
|
|
19
19
|
from rung.drafting import tla as tla_draft
|
|
20
20
|
|
|
21
21
|
|
|
@@ -160,12 +160,20 @@ def cmd_draft(args, out: Output) -> int:
|
|
|
160
160
|
check_id = args.id or f"{_slug(rel)}-{args.kind}"
|
|
161
161
|
target = root / (args.out or f"spec/{args.kind}/{_slug(rel)}")
|
|
162
162
|
target.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
sql = sql_context.gather(root, sources)
|
|
164
|
+
if sql.files:
|
|
165
|
+
out.line(f"Added the SQL that defines {', '.join(sql.defined)} to the drafting context:")
|
|
166
|
+
for f in sql.files:
|
|
167
|
+
out.detail(f"added {f}", indent=2)
|
|
168
|
+
for w in sql.warnings:
|
|
169
|
+
out.line(out.style(f"WARNING {w}", "yellow"))
|
|
163
170
|
out.line(f"Drafting {args.kind} for {', '.join(rel)} with {drafted_by(cfg, provider)} ...")
|
|
164
171
|
|
|
165
172
|
drafter = tla_draft.draft if args.kind == "tla" else lean_draft.draft
|
|
166
|
-
result = drafter(provider, root, sources, target, intent=args.intent or "")
|
|
173
|
+
result = drafter(provider, root, sources, target, intent=args.intent or "", sql=sql)
|
|
167
174
|
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]
|
|
175
|
+
"files": [str(f.relative_to(root)) for f in result.files], "context": result.context,
|
|
176
|
+
"warnings": result.warnings}) # fmt: skip
|
|
169
177
|
out.line(f"{'OK' if result.ok else 'FAILED'} after {result.rounds} round(s): {result.message}")
|
|
170
178
|
out.detail(f"drafted by {drafted_by(cfg, provider)}", indent=2)
|
|
171
179
|
if result.notes:
|
|
@@ -173,6 +181,11 @@ def cmd_draft(args, out: Output) -> int:
|
|
|
173
181
|
out.detail(result.notes, indent=2)
|
|
174
182
|
for f in result.files:
|
|
175
183
|
out.detail(f"wrote {f.relative_to(root)}", indent=2)
|
|
184
|
+
for w in result.warnings:
|
|
185
|
+
if w not in sql.warnings: # those were shown before drafting
|
|
186
|
+
out.detail(w, indent=2)
|
|
187
|
+
if sql.missing and result.ok:
|
|
188
|
+
out.detail(f"this result assumes each call to {', '.join(sql.missing)} is atomic", indent=2)
|
|
176
189
|
if result.trace:
|
|
177
190
|
out.line("\nCounterexample:")
|
|
178
191
|
out.detail(result.trace, indent=2)
|
|
@@ -180,12 +193,13 @@ def cmd_draft(args, out: Output) -> int:
|
|
|
180
193
|
if not result.ok:
|
|
181
194
|
return GATE_FAILED
|
|
182
195
|
|
|
196
|
+
covered = rel + [f for f in result.context if f not in rel] # a changed SQL definition stales the verdict too
|
|
183
197
|
if args.kind == "tla":
|
|
184
198
|
spec = next(f for f in result.files if f.suffix == ".tla")
|
|
185
199
|
cfg_file = next(f for f in result.files if f.suffix == ".cfg")
|
|
186
200
|
options = {"spec": spec.relative_to(root).as_posix(), "config": cfg_file.relative_to(root).as_posix(),
|
|
187
201
|
"drafted_by": drafted_by(cfg, provider)} # fmt: skip
|
|
188
|
-
add_check(cfg, Check(check_id, "tla",
|
|
202
|
+
add_check(cfg, Check(check_id, "tla", covered, options), out)
|
|
189
203
|
else:
|
|
190
204
|
project = target.relative_to(root).as_posix()
|
|
191
205
|
add_check(
|
|
@@ -205,7 +219,7 @@ def cmd_draft(args, out: Output) -> int:
|
|
|
205
219
|
"impl": f"python3 {project}/drt_adapter.py", "alphabet": result.alphabet,
|
|
206
220
|
"cases": 2000, "max_len": 8, "specs": [f"{project}/**/*.lean", f"{project}/drt_adapter.py"],
|
|
207
221
|
} # fmt: skip
|
|
208
|
-
add_check(cfg, Check(f"{check_id}-drt", "drt",
|
|
222
|
+
add_check(cfg, Check(f"{check_id}-drt", "drt", covered, options), out)
|
|
209
223
|
enforcing = [check_id] if args.kind == "tla" else [check_id, f"{check_id}-drt"]
|
|
210
224
|
added = laws.propose(root, result.laws, enforcing)
|
|
211
225
|
if added:
|