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.
Files changed (120) hide show
  1. {rung_cli-0.1.2 → rung_cli-0.1.4}/.claude-plugin/plugin.json +1 -1
  2. {rung_cli-0.1.2 → rung_cli-0.1.4}/CHANGELOG.md +48 -1
  3. {rung_cli-0.1.2 → rung_cli-0.1.4}/PKG-INFO +41 -6
  4. {rung_cli-0.1.2 → rung_cli-0.1.4}/README.md +40 -5
  5. {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/CONFIG.md +6 -0
  6. {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/DESIGN.md +6 -0
  7. {rung_cli-0.1.2 → rung_cli-0.1.4}/pyproject.toml +1 -1
  8. {rung_cli-0.1.2 → rung_cli-0.1.4}/skills/setup/SKILL.md +1 -1
  9. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/__init__.py +1 -1
  10. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/presets.py +3 -1
  11. rung_cli-0.1.4/src/rung/checks/prove.py +271 -0
  12. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/draft.py +19 -5
  13. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/run.py +4 -0
  14. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/git.py +10 -0
  15. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/common.py +39 -0
  16. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/lean.py +15 -4
  17. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/property.py +16 -7
  18. rung_cli-0.1.4/src/rung/drafting/routes.py +196 -0
  19. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/runtime.py +10 -3
  20. rung_cli-0.1.4/src/rung/drafting/sql_context.py +170 -0
  21. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/tla.py +27 -5
  22. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/lean.md +3 -1
  23. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/property.md +2 -0
  24. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/runtime.md +7 -0
  25. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/tla.md +4 -0
  26. rung_cli-0.1.4/tests/test_drafting_context.py +292 -0
  27. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_property.py +4 -0
  28. rung_cli-0.1.4/tests/test_prove.py +157 -0
  29. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_scan_checker.py +28 -0
  30. {rung_cli-0.1.2 → rung_cli-0.1.4}/uv.lock +1 -1
  31. rung_cli-0.1.2/src/rung/checks/prove.py +0 -125
  32. rung_cli-0.1.2/tests/test_prove.py +0 -85
  33. {rung_cli-0.1.2 → rung_cli-0.1.4}/.claude-plugin/marketplace.json +0 -0
  34. {rung_cli-0.1.2 → rung_cli-0.1.4}/.gitignore +0 -0
  35. {rung_cli-0.1.2 → rung_cli-0.1.4}/CODE_OF_CONDUCT.md +0 -0
  36. {rung_cli-0.1.2 → rung_cli-0.1.4}/CONTRIBUTING.md +0 -0
  37. {rung_cli-0.1.2 → rung_cli-0.1.4}/LICENSE +0 -0
  38. {rung_cli-0.1.2 → rung_cli-0.1.4}/SECURITY.md +0 -0
  39. {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/RECIPES.md +0 -0
  40. {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/RESEARCH.md +0 -0
  41. {rung_cli-0.1.2 → rung_cli-0.1.4}/docs/ROADMAP.md +0 -0
  42. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/LAWS.md +0 -0
  43. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/README.md +0 -0
  44. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/__init__.py +0 -0
  45. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/drt.py +0 -0
  46. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/leasequeue/queue.py +0 -0
  47. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/rung.toml +0 -0
  48. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/LeaseModel.lean +0 -0
  49. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/Main.lean +0 -0
  50. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lake-manifest.json +0 -0
  51. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lakefile.toml +0 -0
  52. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/lean/lean-toolchain +0 -0
  53. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/tla/LeaseQueue.cfg +0 -0
  54. {rung_cli-0.1.2 → rung_cli-0.1.4}/examples/lease-queue/spec/tla/LeaseQueue.tla +0 -0
  55. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/__main__.py +0 -0
  56. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/__init__.py +0 -0
  57. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/hooks.py +0 -0
  58. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/agents/install.py +0 -0
  59. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/__init__.py +0 -0
  60. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/plan.py +0 -0
  61. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/report.py +0 -0
  62. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/analysis/survey.py +0 -0
  63. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/__init__.py +0 -0
  64. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/base.py +0 -0
  65. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/command.py +0 -0
  66. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/deps.py +0 -0
  67. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/findings.py +0 -0
  68. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/formal.py +0 -0
  69. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/registry.py +0 -0
  70. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/runner.py +0 -0
  71. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/scan.py +0 -0
  72. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/checks/shortcuts.py +0 -0
  73. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/__init__.py +0 -0
  74. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/__init__.py +0 -0
  75. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/common.py +0 -0
  76. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/manage.py +0 -0
  77. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/commands/setup.py +0 -0
  78. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/cli/output.py +0 -0
  79. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/__init__.py +0 -0
  80. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/config.py +0 -0
  81. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/errors.py +0 -0
  82. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/laws.py +0 -0
  83. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/ledger.py +0 -0
  84. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/proc.py +0 -0
  85. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/core/redact.py +0 -0
  86. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/__init__.py +0 -0
  87. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/providers.py +0 -0
  88. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/drafting/sandbox.py +0 -0
  89. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/__init__.py +0 -0
  90. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/drt.py +0 -0
  91. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/lean.py +0 -0
  92. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/mutate.py +0 -0
  93. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/sarif.py +0 -0
  94. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/shortcuts.py +0 -0
  95. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/engines/tla.py +0 -0
  96. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/__init__.py +0 -0
  97. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/rules.md +0 -0
  98. {rung_cli-0.1.2 → rung_cli-0.1.4}/src/rung/guides/verify.md +0 -0
  99. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/__init__.py +0 -0
  100. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/conftest.py +0 -0
  101. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/gitleaks.sarif +0 -0
  102. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/osv.sarif +0 -0
  103. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/fixtures/sarif/zizmor.sarif +0 -0
  104. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_agents_and_report.py +0 -0
  105. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_cli.py +0 -0
  106. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_config.py +0 -0
  107. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_deps.py +0 -0
  108. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_draft.py +0 -0
  109. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_drt.py +0 -0
  110. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_example_integration.py +0 -0
  111. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_gitutil.py +0 -0
  112. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_laws.py +0 -0
  113. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_lean.py +0 -0
  114. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_ledger.py +0 -0
  115. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_release_consistency.py +0 -0
  116. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_review_regressions.py +0 -0
  117. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_sarif_and_findings.py +0 -0
  118. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_shortcuts.py +0 -0
  119. {rung_cli-0.1.2 → rung_cli-0.1.4}/tests/test_survey_and_agents.py +0 -0
  120. {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.2",
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.2 (unreleased)
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.2
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.2
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.2
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.2
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.2
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.2"
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"
@@ -75,7 +75,7 @@ jobs:
75
75
  with:
76
76
  fetch-depth: 0
77
77
  persist-credentials: false
78
- - uses: CalQuity/rung@v0.1.2
78
+ - uses: CalQuity/rung@v0.1.4
79
79
  ```
80
80
 
81
81
  Install whatever the checks need (language runtimes, scanners) in steps before `CalQuity/rung`.
@@ -1,3 +1,3 @@
1
1
  """Rung: an opinionated verification ladder for software written with coding agents."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -43,7 +43,9 @@ PRESETS: dict[str, Preset] = {
43
43
  Preset(
44
44
  "osv-scanner",
45
45
  "dependencies with known vulnerabilities",
46
- "osv-scanner scan source -r . -f sarif --output-file {out}",
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]}) # fmt: skip
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", rel, options), out)
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", rel, options), out)
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: