@codacy/verity-cli 0.34.0-experimental.dc0010f → 0.34.0
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.
- package/CHANGELOG.md +77 -0
- package/bin/verity.js +101 -70
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -64,6 +64,78 @@ installs are untouched.
|
|
|
64
64
|
which is the ordinary case for someone whose first Verity install *is* the
|
|
65
65
|
plugin.
|
|
66
66
|
|
|
67
|
+
## [0.34.0] — 2026-09-30
|
|
68
|
+
|
|
69
|
+
**Formal verification at push: Verity can now prove a bug is real before it
|
|
70
|
+
blocks — and the code map and the knowledge loop tell the reviewer more about
|
|
71
|
+
the code around a change.**
|
|
72
|
+
|
|
73
|
+
### 🧮 Formal verification, opt-in
|
|
74
|
+
|
|
75
|
+
A third review at `git push`, next to the one-shot and the agent. It picks the
|
|
76
|
+
riskiest function the push changed, has the Verity service write a small Lean 4
|
|
77
|
+
model of it, checks the model on your machine, and replays any counterexample
|
|
78
|
+
on the real code in an isolated container with the network off. **Only a
|
|
79
|
+
reproduced bug blocks;** a counterexample that does not reproduce on the real
|
|
80
|
+
code is reported, never enforced. Most pushes get no model at all.
|
|
81
|
+
|
|
82
|
+
- **Off until you say yes.** `verity init` asks once, on a terminal, with "no"
|
|
83
|
+
as the default, and remembers either answer. `--yes` and headless runs are not
|
|
84
|
+
asked and never probe for a toolchain. `verity config formal push` turns it on
|
|
85
|
+
for a checkout, `verity config formal off` turns it off. It needs you to be
|
|
86
|
+
signed in: ranking, model writing and replay run on the Verity service. The
|
|
87
|
+
function's source is sent for the replay; no code is stored.
|
|
88
|
+
- **`verity lean install | status | remove`** manages the pinned official Lean 4
|
|
89
|
+
release under `~/.verity/lean` (≈800 MB download, 2.7 GB on disk). The
|
|
90
|
+
download is checksum-verified, abandoned after 60 s with nothing received,
|
|
91
|
+
and refused up front when the disk is short. An elan install is used when it
|
|
92
|
+
already has the pinned toolchain; Verity never makes elan download one.
|
|
93
|
+
- **On demand:** `verity prove candidates | generate | check | replay | auto`,
|
|
94
|
+
and the `/verity-prove <worry>` skill, where the words after the command are
|
|
95
|
+
what you want checked. Under the plugin, `/verity:verity-prove` arrives with
|
|
96
|
+
the plugin's next release.
|
|
97
|
+
- **A finding holds until the code changes.** Pushing the same bytes again
|
|
98
|
+
re-blocks without a new model. `verity prove waive <file>` accepts the risk
|
|
99
|
+
for the file's current bytes, and voids when they change.
|
|
100
|
+
- **Yours, never the repository's.** A `.verity/config.json` that git tracks,
|
|
101
|
+
or that is reached through a symlink, cannot switch formal verification on —
|
|
102
|
+
`verity doctor`, `verity config formal` and `verity init` say why it is off.
|
|
103
|
+
Findings and spend records a repository ships are ignored, and Lean never runs
|
|
104
|
+
on a `.verity/proofs/` workspace the repository supplies.
|
|
105
|
+
- **Admins can switch it off** per project, organization or globally with the
|
|
106
|
+
`formal` flag in the dashboard; the push then reports `disabled-by-server`.
|
|
107
|
+
|
|
108
|
+
### 🗺️ The code map
|
|
109
|
+
|
|
110
|
+
The CLI now parses TypeScript, TSX, JavaScript, Python, Go and Java with bundled
|
|
111
|
+
tree-sitter grammars (about 6 MB added to the package).
|
|
112
|
+
|
|
113
|
+
- **The reviewer sees what else touches the same state.** At commit and push,
|
|
114
|
+
the review's context names the other functions that write the state a change
|
|
115
|
+
writes, and the agent reviewer can ask for a function's callers and
|
|
116
|
+
definitions. Formal verification models the exact function, not a guess at
|
|
117
|
+
its extent.
|
|
118
|
+
- **Reads use the bytes under review:** at a push, the pushed commit; at a
|
|
119
|
+
commit, the staged files — not whatever is in the working tree.
|
|
120
|
+
- **Paths the project denies are never shown** by the code tools or in the
|
|
121
|
+
code-map context.
|
|
122
|
+
|
|
123
|
+
### 🧠 The knowledge loop
|
|
124
|
+
|
|
125
|
+
- **The reviewer says what it did with each node it was shown** — applied,
|
|
126
|
+
violated, irrelevant or unclear, each tied to a line in the change — and
|
|
127
|
+
ranking learns from it which nodes help. One scorer now chooses the nodes for
|
|
128
|
+
both the review and your agent's prompt.
|
|
129
|
+
- **A known rule said back to the agent.** When a change breaks a rule the
|
|
130
|
+
project has already learned, Verity can tell the agent which one. This ships
|
|
131
|
+
in shadow: the decision is made and recorded, and nothing is sent.
|
|
132
|
+
- **Fewer, better nodes.** The extractor's rubric was rewritten from 143
|
|
133
|
+
human-graded nodes; it no longer records what the code already says, and it
|
|
134
|
+
checks each new node against the nearest existing ones before adding it.
|
|
135
|
+
- **A failed diff in `verity memory` sync no longer drops a node edit you made.**
|
|
136
|
+
- **A failed knowledge lookup no longer fails a review.** The review runs
|
|
137
|
+
without knowledge and records that it did.
|
|
138
|
+
|
|
67
139
|
### 🎯 `/verity:init` does everything `verity init` does, in the session
|
|
68
140
|
|
|
69
141
|
Under the plugin, setup used to stop short of the terminal flow: `/verity:init`
|
|
@@ -89,6 +161,11 @@ sign-in meant leaving Claude Code for a terminal.
|
|
|
89
161
|
- **`verity telemetry install|uninstall` update `.verity/setup.json`**, so a
|
|
90
162
|
request deferred until sign-in stops being reported as still to do.
|
|
91
163
|
|
|
164
|
+
### 🔧 Fixed
|
|
165
|
+
|
|
166
|
+
- **A swapped `.verity` directory cannot redirect a write.** File writes check
|
|
167
|
+
the file they opened is the one they meant to write, after opening it.
|
|
168
|
+
|
|
92
169
|
## [0.33.3] — 2026-09-24
|
|
93
170
|
|
|
94
171
|
**The knowledge seed runs once per project, not once per checkout.**
|
package/bin/verity.js
CHANGED
|
@@ -14628,7 +14628,7 @@ var SECURITY_PATTERNS = [
|
|
|
14628
14628
|
/Dockerfile/
|
|
14629
14629
|
];
|
|
14630
14630
|
var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
|
|
14631
|
-
var DEFAULT_SERVICE_URL = "
|
|
14631
|
+
var DEFAULT_SERVICE_URL = "".length > 0 ? "" : PROD_SERVICE_URL;
|
|
14632
14632
|
var GITHUB_CLIENT_ID = "Iv23li88HxAi3ZrbYzWh";
|
|
14633
14633
|
var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
14634
14634
|
var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
@@ -17221,7 +17221,7 @@ function readConfigFile(path) {
|
|
|
17221
17221
|
return {
|
|
17222
17222
|
git_moments: [...new Set(moments)],
|
|
17223
17223
|
...source2 ? { git_moments_source: source2 } : {},
|
|
17224
|
-
...raw.formal === "push" ? { formal:
|
|
17224
|
+
...raw.formal === "push" || raw.formal === "off" ? { formal: raw.formal } : {}
|
|
17225
17225
|
};
|
|
17226
17226
|
} catch {
|
|
17227
17227
|
return DEFAULTS;
|
|
@@ -24152,7 +24152,7 @@ function registerConfigCommands(program2) {
|
|
|
24152
24152
|
config.command("formal [state]").description('Get or set formal verification at push: "push" or "off" (needs Lean \u2014 verity lean status)').action((state) => {
|
|
24153
24153
|
if (state === void 0) {
|
|
24154
24154
|
const cfg = readProjectConfig();
|
|
24155
|
-
process.stdout.write((cfg.formal
|
|
24155
|
+
process.stdout.write((cfg.formal === "push" ? "push" : "off") + "\n");
|
|
24156
24156
|
if (cfg.formal_refused) printWarn(formalRefusalMessage(cfg.formal_refused));
|
|
24157
24157
|
return;
|
|
24158
24158
|
}
|
|
@@ -24160,7 +24160,7 @@ function registerConfigCommands(program2) {
|
|
|
24160
24160
|
printError(`Unrecognised state: ${state}. Use "push" or "off".`);
|
|
24161
24161
|
process.exit(1);
|
|
24162
24162
|
}
|
|
24163
|
-
writeProjectConfig({ formal: state === "push" ? "push" :
|
|
24163
|
+
writeProjectConfig({ formal: state === "push" ? "push" : "off" });
|
|
24164
24164
|
const refused2 = state === "push" ? readProjectConfig().formal_refused : void 0;
|
|
24165
24165
|
if (refused2) {
|
|
24166
24166
|
printWarn(`Formal verification at push: still off \u2014 ${formalRefusalMessage(refused2)}`);
|
|
@@ -27073,7 +27073,7 @@ function channelSilence(input) {
|
|
|
27073
27073
|
// src/lib/cli-version.ts
|
|
27074
27074
|
function cliVersion() {
|
|
27075
27075
|
try {
|
|
27076
|
-
return true ? "0.34.0
|
|
27076
|
+
return true ? "0.34.0" : "dev";
|
|
27077
27077
|
} catch {
|
|
27078
27078
|
return "dev";
|
|
27079
27079
|
}
|
|
@@ -33869,6 +33869,11 @@ async function rankViaServer(candidates2, ctx, auth2) {
|
|
|
33869
33869
|
cmd: "prove-rank"
|
|
33870
33870
|
});
|
|
33871
33871
|
if (!res.ok) return { ok: false, ranker: "luna", error: res.error, ranked: fallback };
|
|
33872
|
+
const off = res.data;
|
|
33873
|
+
if (off?.enabled === false) {
|
|
33874
|
+
const why = typeof off.reason === "string" ? off.reason.slice(0, 40) : "disabled";
|
|
33875
|
+
return { ok: false, ranker: "luna", error: `formal is off for this project (${why})`, ranked: fallback, disabled: why };
|
|
33876
|
+
}
|
|
33872
33877
|
const items = Array.isArray(res.data?.items) ? res.data.items : [];
|
|
33873
33878
|
return { ok: true, ranker: "luna", ranked: applyAnnotations(candidates2, items), costUsd: typeof res.data?.cost_usd === "number" ? res.data.cost_usd : null };
|
|
33874
33879
|
}
|
|
@@ -34593,6 +34598,8 @@ async function generateModel(target, opts) {
|
|
|
34593
34598
|
});
|
|
34594
34599
|
if (!res.ok) return { ok: false, error: res.error };
|
|
34595
34600
|
const d = res.data ?? {};
|
|
34601
|
+
const off = d;
|
|
34602
|
+
if (off.enabled === false) return { ok: false, error: `formal is off for this project (${typeof off.reason === "string" ? off.reason.slice(0, 40) : "disabled"})` };
|
|
34596
34603
|
if (d.error && typeof d.error === "object") return { ok: false, error: `${d.error.code ?? "SERVICE_ERROR"}: ${d.error.message ?? "the service failed"}` };
|
|
34597
34604
|
const usage2 = d.usage && typeof d.usage === "object" ? { inputTokens: Number(d.usage.inputTokens ?? 0), outputTokens: Number(d.usage.outputTokens ?? 0) } : null;
|
|
34598
34605
|
return acceptAnswer(d, usage2, typeof d.cost_usd === "number" ? d.cost_usd : null);
|
|
@@ -34772,8 +34779,7 @@ Rules:
|
|
|
34772
34779
|
5. Write the harness as ONE file and run it in the SAME shell call that writes it (a heredoc followed by the run). TypeScript: node --experimental-strip-types harness.ts (detect runtimes only if that fails: node --version, python3 --version); if strip-types is unavailable, remove type annotations ONLY on lines that are not part of the function under test's logic, and say so. Python: python3 harness.py.
|
|
34773
34780
|
6. Nothing you run can reach the network. Do not try.
|
|
34774
34781
|
7. Keep it under about eight shell calls. Output tokens are time.
|
|
34775
|
-
8. Answer with the JSON object only: verdict (reproduced | diverged | unreplayable), reason (2-4 sentences: what you ran, what the real code did, and how that compares to the trace's final state), harness_files (every file you wrote: path and its line count \u2014 do NOT paste contents, your shell calls already carry them), executed_output (the last relevant stdout, at most 2000 characters)
|
|
34776
|
-
9. impact \u2014 AFTER the verdict, and never a reason to change it: what the violation you ran costs a user of this code, the most serious one the run showed. money: a balance, charge, refund, price or stock is wrong. access: someone gets access they should not have, or an auth check is bypassed. data: a user's records are lost, corrupted or wrongly duplicated. external: an effect outside the system happens twice or never (a charge, an email, a shipment, a webhook). count: a counter, metric or statistic is off. cache: a cached or derived value is stale until it refreshes. internal: the program's own bookkeeping is off, with no effect a user would see. none: the verdict is not reproduced.
|
|
34782
|
+
8. Answer with the JSON object only: verdict (reproduced | diverged | unreplayable), reason (2-4 sentences: what you ran, what the real code did, and how that compares to the trace's final state), harness_files (every file you wrote: path and its line count \u2014 do NOT paste contents, your shell calls already carry them), executed_output (the last relevant stdout, at most 2000 characters).
|
|
34777
34783
|
|
|
34778
34784
|
"reproduced" means the real code, driven by the trace, reached the property violation the model describes. "diverged" means it did not \u2014 the model is wrong about the code, which is a useful answer. "unreplayable" means you could not run the real function faithfully; say why.`;
|
|
34779
34785
|
var REPLAY_SCHEMA = {
|
|
@@ -34791,10 +34797,9 @@ var REPLAY_SCHEMA = {
|
|
|
34791
34797
|
required: ["path", "lines"]
|
|
34792
34798
|
}
|
|
34793
34799
|
},
|
|
34794
|
-
executed_output: { type: "string" }
|
|
34795
|
-
impact: { type: "string", enum: ["money", "access", "data", "external", "count", "cache", "internal", "none"] }
|
|
34800
|
+
executed_output: { type: "string" }
|
|
34796
34801
|
},
|
|
34797
|
-
required: ["verdict", "reason", "harness_files", "executed_output"
|
|
34802
|
+
required: ["verdict", "reason", "harness_files", "executed_output"]
|
|
34798
34803
|
};
|
|
34799
34804
|
function renderBriefInput(brief) {
|
|
34800
34805
|
const p = brief.property;
|
|
@@ -34821,8 +34826,6 @@ function renderBriefInput(brief) {
|
|
|
34821
34826
|
}
|
|
34822
34827
|
return parts2.join("\n");
|
|
34823
34828
|
}
|
|
34824
|
-
var IMPACTS = ["money", "access", "data", "external", "count", "cache", "internal", "none"];
|
|
34825
|
-
var isImpact = (v) => typeof v === "string" && IMPACTS.includes(v);
|
|
34826
34829
|
function stringsIn(value, out2 = [], depth = 0) {
|
|
34827
34830
|
if (depth > 8) return out2;
|
|
34828
34831
|
if (typeof value === "string") out2.push(value);
|
|
@@ -34864,8 +34867,7 @@ function parseReplayOutput(output) {
|
|
|
34864
34867
|
verdict,
|
|
34865
34868
|
reason: typeof o.reason === "string" ? o.reason.slice(0, 2e3) : "",
|
|
34866
34869
|
harness_files: Array.isArray(o.harness_files) ? o.harness_files.filter((f) => typeof f.path === "string").map((f) => ({ path: String(f.path).slice(0, 200), lines: Number.isInteger(f.lines) ? f.lines : 0 })) : [],
|
|
34867
|
-
executed_output: typeof o.executed_output === "string" ? o.executed_output.slice(0, 2e3) : ""
|
|
34868
|
-
impact: isImpact(o.impact) ? o.impact : "none"
|
|
34870
|
+
executed_output: typeof o.executed_output === "string" ? o.executed_output.slice(0, 2e3) : ""
|
|
34869
34871
|
};
|
|
34870
34872
|
}
|
|
34871
34873
|
} catch {
|
|
@@ -34873,10 +34875,10 @@ function parseReplayOutput(output) {
|
|
|
34873
34875
|
}
|
|
34874
34876
|
return { claim, shellCalls, executedCalls, shellTexts, outputTexts };
|
|
34875
34877
|
}
|
|
34876
|
-
var RESULT_LINE = /REPLAY_RESULT=(reproduced|diverged)\b
|
|
34878
|
+
var RESULT_LINE = /REPLAY_RESULT=(reproduced|diverged)\b/g;
|
|
34877
34879
|
function printedResult(outputTexts) {
|
|
34878
34880
|
let last = null;
|
|
34879
|
-
for (const t of outputTexts) for (const m of t.matchAll(
|
|
34881
|
+
for (const t of outputTexts) for (const m of t.matchAll(RESULT_LINE)) last = m[1];
|
|
34880
34882
|
return last;
|
|
34881
34883
|
}
|
|
34882
34884
|
var RESULT_MISSING = "the harness never printed REPLAY_RESULT=reproduced on its own stdout \u2014 the verdict was the model's word, not the run's";
|
|
@@ -35066,7 +35068,6 @@ async function runReplayInContainer(brief, opts) {
|
|
|
35066
35068
|
...judged,
|
|
35067
35069
|
executedCalls,
|
|
35068
35070
|
shellCalls,
|
|
35069
|
-
...judged.verdict === "reproduced" && parsed.claim ? { impact: parsed.claim.impact } : {},
|
|
35070
35071
|
executedOutput: parsed.claim?.executed_output ?? "",
|
|
35071
35072
|
harnessFiles: parsed.claim?.harness_files ?? [],
|
|
35072
35073
|
usage,
|
|
@@ -35113,6 +35114,11 @@ async function runReplayViaServer(brief, auth2, budgetMs = 24e4) {
|
|
|
35113
35114
|
return { claimed: null, verdict: "unverified", reason: "", downgrade: res.error, executedCalls: 0, shellCalls: 0, verbatim: none, executedOutput: "", harnessFiles: [], usage: null, durationMs: Date.now() - started, containerId: null, error: res.error };
|
|
35114
35115
|
}
|
|
35115
35116
|
const d = res.data ?? {};
|
|
35117
|
+
if (d.enabled === false) {
|
|
35118
|
+
const why = d.reason;
|
|
35119
|
+
const msg = `formal is off for this project (${typeof why === "string" ? why.slice(0, 40) : "disabled"})`;
|
|
35120
|
+
return { claimed: null, verdict: "unverified", reason: "", downgrade: msg, executedCalls: 0, shellCalls: 0, verbatim: none, executedOutput: "", harnessFiles: [], usage: null, durationMs: Date.now() - started, containerId: null, error: msg };
|
|
35121
|
+
}
|
|
35116
35122
|
if (d.error && typeof d.error === "object") {
|
|
35117
35123
|
const e = d.error;
|
|
35118
35124
|
const msg = `${e.code ?? "SERVICE_ERROR"}: ${e.message ?? "the service failed"}`;
|
|
@@ -35125,7 +35131,6 @@ async function runReplayViaServer(brief, auth2, budgetMs = 24e4) {
|
|
|
35125
35131
|
verdict: verdict === "reproduced" || verdict === "diverged" || verdict === "unreplayable" ? verdict : "unverified",
|
|
35126
35132
|
reason: typeof d.reason === "string" ? d.reason : "",
|
|
35127
35133
|
downgrade: typeof d.downgrade === "string" ? d.downgrade : null,
|
|
35128
|
-
...isImpact(d.impact) ? { impact: d.impact } : {},
|
|
35129
35134
|
executedCalls: Number(d.executed_calls ?? 0),
|
|
35130
35135
|
shellCalls: Number(d.shell_calls ?? 0),
|
|
35131
35136
|
verbatim: { covered: Number(v.covered ?? 0), total: Number(v.total ?? 0), ratio: Number(v.ratio ?? 0), basis: v.basis === "symbol" || v.basis === "file" ? v.basis : "none" },
|
|
@@ -35910,9 +35915,10 @@ function verityLeanHome(env = process.env) {
|
|
|
35910
35915
|
function findToolchain(env = process.env) {
|
|
35911
35916
|
const managedBin = (0, import_node_path43.join)(verityLeanHome(env), LEAN_TOOLCHAIN.replace(/^leanprover\/lean4:/, ""), "bin");
|
|
35912
35917
|
const elanBin = (0, import_node_path43.join)(env.HOME ?? (0, import_node_os9.homedir)(), ".elan", "bin");
|
|
35913
|
-
const path = [managedBin, elanBin, env.PATH ?? ""].filter(Boolean).join(import_node_path43.delimiter);
|
|
35914
|
-
const merged = { ...env, PATH: path, ELAN_TOOLCHAIN: env.ELAN_TOOLCHAIN ?? LEAN_TOOLCHAIN };
|
|
35915
35918
|
const elanHas = (0, import_node_fs58.existsSync)((0, import_node_path43.join)(env.HOME ?? (0, import_node_os9.homedir)(), ".elan", "toolchains", LEAN_TOOLCHAIN.replace("/", "--").replace(":", "---")));
|
|
35919
|
+
const userPath = (env.PATH ?? "").split(import_node_path43.delimiter).filter((d) => d && (elanHas || (0, import_node_path43.resolve)(d) !== (0, import_node_path43.resolve)(elanBin)));
|
|
35920
|
+
const path = [managedBin, ...elanHas ? [elanBin] : [], ...userPath].join(import_node_path43.delimiter);
|
|
35921
|
+
const merged = { ...env, PATH: path, ELAN_TOOLCHAIN: env.ELAN_TOOLCHAIN ?? LEAN_TOOLCHAIN };
|
|
35916
35922
|
const exe = process.platform === "win32" ? "lake.exe" : "lake";
|
|
35917
35923
|
const candidates2 = [(0, import_node_path43.join)(managedBin, exe), ...elanHas ? [(0, import_node_path43.join)(elanBin, exe)] : [], exe];
|
|
35918
35924
|
for (const candidate of candidates2) {
|
|
@@ -36107,33 +36113,18 @@ function writeResult(root, report) {
|
|
|
36107
36113
|
function isCrashTrace(trace) {
|
|
36108
36114
|
return trace.some((s) => /\b(crash\w*|kill\w*|restart\w*|interrupt\w*|power|abort\w*|process (dies|exit\w*)|lost (reply|response))\b/i.test(s));
|
|
36109
36115
|
}
|
|
36110
|
-
var SEVERITY_BY_IMPACT = {
|
|
36111
|
-
money: "high",
|
|
36112
|
-
access: "high",
|
|
36113
|
-
data: "high",
|
|
36114
|
-
external: "high",
|
|
36115
|
-
count: "medium",
|
|
36116
|
-
cache: "medium",
|
|
36117
|
-
internal: "medium",
|
|
36118
|
-
// A reproduced finding always has a real code; 'none' here is a malformed answer, so it blocks.
|
|
36119
|
-
none: "high"
|
|
36120
|
-
};
|
|
36121
|
-
function formalSeverity(f) {
|
|
36122
|
-
return f.impact && SEVERITY_BY_IMPACT[f.impact] || "high";
|
|
36123
|
-
}
|
|
36124
36116
|
function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
36125
|
-
if (!outcome) return { blocking: 0
|
|
36117
|
+
if (!outcome) return { blocking: 0 };
|
|
36126
36118
|
const norm2 = (p) => p.replace(/\\/g, "/");
|
|
36127
36119
|
const pushed = new Set(pushedFiles.map(norm2));
|
|
36128
|
-
const
|
|
36129
|
-
if (
|
|
36130
|
-
const blocking = reproduced.filter((f) => formalSeverity(f) === "high");
|
|
36120
|
+
const blocking = outcome.findings.filter((f) => f.verdict === "reproduced" && !f.waived && pushed.has(norm2(f.file)));
|
|
36121
|
+
if (blocking.length === 0) return { blocking: 0 };
|
|
36131
36122
|
const existing = Array.isArray(response.findings) ? response.findings : [];
|
|
36132
36123
|
response.findings = [
|
|
36133
36124
|
...existing,
|
|
36134
|
-
...
|
|
36125
|
+
...blocking.map((f) => ({
|
|
36135
36126
|
pattern_id: "formal-counterexample",
|
|
36136
|
-
severity:
|
|
36127
|
+
severity: "high",
|
|
36137
36128
|
scope: "change",
|
|
36138
36129
|
file: f.file,
|
|
36139
36130
|
line: outcome.target?.lines?.from ?? null,
|
|
@@ -36141,15 +36132,14 @@ function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
|
36141
36132
|
// as a title it read backwards (e2e, 2026-09-26). A trace that needs a
|
|
36142
36133
|
// crash between two writes is labelled: reproducing it means injecting
|
|
36143
36134
|
// the crash, so it is a crash-consistency finding, not a race.
|
|
36144
|
-
title: `Formal: ${terminalText(f.symbol, 128)} breaks "${terminalText(f.desc, 280)}"${isCrashTrace(f.trace) ? " after a crash (crash consistency)" : ""} \u2014 reproduced on the real code
|
|
36135
|
+
title: `Formal: ${terminalText(f.symbol, 128)} breaks "${terminalText(f.desc, 280)}"${isCrashTrace(f.trace) ? " after a crash (crash consistency)" : ""} \u2014 reproduced on the real code`,
|
|
36145
36136
|
description: terminalText(f.reason ?? "", 2e3),
|
|
36146
36137
|
fix: { description: `Trace: ${terminalText(f.trace.join(" \u2192 ") || "(initial state)", 2e3)}. Replay: ${terminalText(f.reason ?? "reproduced in the container", 2e3)}` },
|
|
36147
36138
|
source: "formal"
|
|
36148
36139
|
}))
|
|
36149
36140
|
];
|
|
36150
|
-
if (
|
|
36151
|
-
|
|
36152
|
-
return { blocking: blocking.length, warning: reproduced.length - blocking.length };
|
|
36141
|
+
if (response.gate_decision !== "FAIL") response.gate_decision = "FAIL";
|
|
36142
|
+
return { blocking: blocking.length };
|
|
36153
36143
|
}
|
|
36154
36144
|
|
|
36155
36145
|
// src/lib/prove/scalpel.ts
|
|
@@ -36468,9 +36458,7 @@ async function runFormalPhase(input) {
|
|
|
36468
36458
|
symbol: f.symbol,
|
|
36469
36459
|
verdict: "reproduced",
|
|
36470
36460
|
reason: f.waived ? `waived ${f.waived.at.slice(0, 10)}: ${f.waived.reason} (standing since ${f.at.slice(0, 10)}; the file has not changed)` : `${f.reason ?? "reproduced on the real code"} (standing since ${f.at.slice(0, 10)}; the file has not changed)`,
|
|
36471
|
-
...f.waived ? { waived: true } : {}
|
|
36472
|
-
// A hand-edited file cannot invent a code: unknown values read as absent (HIGH).
|
|
36473
|
-
...isImpact(f.impact) ? { impact: f.impact } : {}
|
|
36461
|
+
...f.waived ? { waived: true } : {}
|
|
36474
36462
|
});
|
|
36475
36463
|
}
|
|
36476
36464
|
}
|
|
@@ -36509,6 +36497,7 @@ async function runFormalPhase(input) {
|
|
|
36509
36497
|
return done({ status: "skipped", skip: "not-eligible", detail: `${best.file}: ${isEligible(best, largePush).why}` });
|
|
36510
36498
|
}
|
|
36511
36499
|
const ranked = await deps.rank(eligible, root, input.changedLines);
|
|
36500
|
+
if (!ranked.ok && ranked.disabled) return done({ status: "skipped", skip: "disabled-by-server", detail: ranked.error });
|
|
36512
36501
|
if (ranked.ok) spend(ranked);
|
|
36513
36502
|
candidates2 = ranked.ranked.slice(0, 5).map((c) => ({ file: c.file, symbol: c.ai?.symbol ?? null, score: c.ai?.score ?? null, criteria: c.ai?.criteria ?? null, reasked: !!c.ai?.reasked, reasons: (c.ai?.reasons ?? []).map((r) => r.slice(0, 200)) }));
|
|
36514
36503
|
if (cancelled()) return done({ status: "skipped", skip: "deadline", detail: "cut off after ranking" });
|
|
@@ -36743,7 +36732,7 @@ async function runFormalPhase(input) {
|
|
|
36743
36732
|
continue;
|
|
36744
36733
|
}
|
|
36745
36734
|
const verdict = r.verdict === "reproduced" ? "reproduced" : r.verdict === "diverged" ? "diverged" : r.verdict === "unreplayable" ? "model-only" : "unverified";
|
|
36746
|
-
findings.push({ ...baseOf(p), verdict, reason: r.reason || r.downgrade || void 0
|
|
36735
|
+
findings.push({ ...baseOf(p), verdict, reason: r.reason || r.downgrade || void 0 });
|
|
36747
36736
|
}
|
|
36748
36737
|
const reproduced = findings.filter((f) => f.verdict === "reproduced");
|
|
36749
36738
|
if (reproduced.length > 0 && !cancelled()) {
|
|
@@ -36751,7 +36740,7 @@ async function runFormalPhase(input) {
|
|
|
36751
36740
|
const others = readPersistedFindings(root).filter((f) => f.file !== top.file);
|
|
36752
36741
|
writePersistedFindings(root, [
|
|
36753
36742
|
...others,
|
|
36754
|
-
...reproduced.map((f) => ({ file: f.file, symbol: f.symbol, sha256: sha2562, property: f.property, desc: f.desc, kind: f.kind, trace: f.trace, model: modelName, reason: f.reason, ...
|
|
36743
|
+
...reproduced.map((f) => ({ file: f.file, symbol: f.symbol, sha256: sha2562, property: f.property, desc: f.desc, kind: f.kind, trace: f.trace, model: modelName, reason: f.reason, ...lines ? { lines } : {}, at: now }))
|
|
36755
36744
|
]);
|
|
36756
36745
|
}
|
|
36757
36746
|
const clean2 = findings.every((f) => f.verdict === "diverged");
|
|
@@ -37491,8 +37480,6 @@ async function runGuard(opts, globals) {
|
|
|
37491
37480
|
rounds: formalOutcome.model?.rounds ?? null,
|
|
37492
37481
|
properties: formalOutcome.properties?.map((p) => `${p.id}:${p.result}`) ?? [],
|
|
37493
37482
|
findings: formalOutcome.findings.map((f) => `${f.property}:${f.verdict}`),
|
|
37494
|
-
// Decision 82: the replay's impact code per reproduced finding; count/cache/internal warn, the rest block.
|
|
37495
|
-
impacts: formalOutcome.findings.filter((f) => f.impact).map((f) => `${f.property}:${f.impact}`),
|
|
37496
37483
|
// Decision 81: what each property rests on (a rule, or "line"), what was
|
|
37497
37484
|
// sent back or dropped as ungrounded, and which counterexamples lack their
|
|
37498
37485
|
// property's trigger (shadow). Ids and rule names only, never code.
|
|
@@ -38802,6 +38789,22 @@ var import_promises18 = require("node:stream/promises");
|
|
|
38802
38789
|
var import_node_stream = require("node:stream");
|
|
38803
38790
|
var RELEASES = "https://releases.lean-lang.org/lean4";
|
|
38804
38791
|
var LEAN_DOWNLOAD_SIZE = "\u2248800 MB download, 2.7 GB on disk";
|
|
38792
|
+
var DOWNLOAD_STALL_MS = 6e4;
|
|
38793
|
+
var LEAN_DISK_NEEDED_BYTES = 3.75 * 1024 ** 3;
|
|
38794
|
+
function freeBytes(dir) {
|
|
38795
|
+
let d = dir;
|
|
38796
|
+
for (let i2 = 0; i2 < 64; i2++) {
|
|
38797
|
+
try {
|
|
38798
|
+
const st = (0, import_node_fs63.statfsSync)(d);
|
|
38799
|
+
return Number(st.bavail) * Number(st.bsize);
|
|
38800
|
+
} catch {
|
|
38801
|
+
const up = (0, import_node_path47.join)(d, "..");
|
|
38802
|
+
if (up === d) return null;
|
|
38803
|
+
d = up;
|
|
38804
|
+
}
|
|
38805
|
+
}
|
|
38806
|
+
return null;
|
|
38807
|
+
}
|
|
38805
38808
|
var LEAN_ASSET_SHA256 = {
|
|
38806
38809
|
"v4.34.0": {
|
|
38807
38810
|
"lean-4.34.0-darwin.zip": "75a2a4b8e3d35680e8f51fae5a42c79702f463dd73ee1520fb61e5a9143e336c",
|
|
@@ -38860,17 +38863,32 @@ function unzip(zip, dest) {
|
|
|
38860
38863
|
}
|
|
38861
38864
|
return { ok: false, error: errors.join("; ") };
|
|
38862
38865
|
}
|
|
38863
|
-
async function download(url, dest, onProgress) {
|
|
38864
|
-
const
|
|
38865
|
-
|
|
38866
|
-
const
|
|
38867
|
-
|
|
38868
|
-
|
|
38869
|
-
|
|
38870
|
-
|
|
38871
|
-
|
|
38872
|
-
|
|
38873
|
-
|
|
38866
|
+
async function download(url, dest, onProgress, stallMs = DOWNLOAD_STALL_MS) {
|
|
38867
|
+
const ctrl = new AbortController();
|
|
38868
|
+
let timer;
|
|
38869
|
+
const arm = () => {
|
|
38870
|
+
clearTimeout(timer);
|
|
38871
|
+
timer = setTimeout(() => ctrl.abort(), stallMs);
|
|
38872
|
+
};
|
|
38873
|
+
arm();
|
|
38874
|
+
try {
|
|
38875
|
+
const res = await fetch(url, { redirect: "follow", signal: ctrl.signal });
|
|
38876
|
+
if (!res.ok || !res.body) throw new Error(`download failed: HTTP ${res.status}`);
|
|
38877
|
+
const total = Number(res.headers.get("content-length")) || null;
|
|
38878
|
+
let seen = 0;
|
|
38879
|
+
const stream = import_node_stream.Readable.fromWeb(res.body);
|
|
38880
|
+
stream.on("data", (chunk) => {
|
|
38881
|
+
arm();
|
|
38882
|
+
seen += chunk.length;
|
|
38883
|
+
onProgress(seen, total);
|
|
38884
|
+
});
|
|
38885
|
+
await (0, import_promises18.pipeline)(stream, (0, import_node_fs63.createWriteStream)(dest), { signal: ctrl.signal });
|
|
38886
|
+
} catch (err2) {
|
|
38887
|
+
if (ctrl.signal.aborted) throw new Error(`download stalled: nothing received for ${Math.round(stallMs / 1e3)} s`);
|
|
38888
|
+
throw err2;
|
|
38889
|
+
} finally {
|
|
38890
|
+
clearTimeout(timer);
|
|
38891
|
+
}
|
|
38874
38892
|
}
|
|
38875
38893
|
function inPlaceProgress(label2, write = (s) => process.stderr.write(s)) {
|
|
38876
38894
|
let active = false;
|
|
@@ -38908,6 +38926,12 @@ async function installLean(opts = {}) {
|
|
|
38908
38926
|
const dir = pinnedVersionDir(env);
|
|
38909
38927
|
const url = `${RELEASES}/${pinnedVersion()}/${asset}`;
|
|
38910
38928
|
const home = verityLeanHome(env);
|
|
38929
|
+
const free = freeBytes(home);
|
|
38930
|
+
const needed = opts.minFreeBytes ?? LEAN_DISK_NEEDED_BYTES;
|
|
38931
|
+
if (free !== null && free < needed) {
|
|
38932
|
+
const gb = (n) => (n / 1024 ** 3).toFixed(1);
|
|
38933
|
+
return { ok: false, error: `not enough disk space for Lean: ${gb(free)} GB free where ${home} lives, needs about ${gb(needed)} GB` };
|
|
38934
|
+
}
|
|
38911
38935
|
const tmp = (0, import_node_path47.join)(home, ".tmp");
|
|
38912
38936
|
(0, import_node_fs63.rmSync)(tmp, { recursive: true, force: true });
|
|
38913
38937
|
(0, import_node_fs63.mkdirSync)(tmp, { recursive: true });
|
|
@@ -39216,8 +39240,10 @@ var INTENSITY_CHOICES = [
|
|
|
39216
39240
|
{ id: "thorough", label: "thorough", hint: "all tools, all rules (~15s)" }
|
|
39217
39241
|
];
|
|
39218
39242
|
var FORMAL_CHOICES = [
|
|
39219
|
-
{ id: "yes", label: "yes", hint: "a Lean model of the riskiest changed function; blocks only on a REPRODUCED counterexample"
|
|
39220
|
-
|
|
39243
|
+
{ id: "yes", label: "yes", hint: "a Lean model of the riskiest changed function; blocks only on a REPRODUCED counterexample" },
|
|
39244
|
+
// No is the default (review of #255): "yes" downloads ≈800 MB and unpacks
|
|
39245
|
+
// 2.7 GB, and a bare Enter must never do that.
|
|
39246
|
+
{ id: "no", label: "no", hint: "enable later with: verity config formal push", recommended: true }
|
|
39221
39247
|
];
|
|
39222
39248
|
var MOMENT_CHOICES = [
|
|
39223
39249
|
{ id: "stop", label: "On stop", hint: "after every agent turn \u2014 fast feedback while you work", recommended: true },
|
|
@@ -39623,16 +39649,20 @@ async function offerFormalVerification(defaultsOnly, pluginMode) {
|
|
|
39623
39649
|
printWarn(` Formal verification: off \u2014 ${formalRefusalMessage(cfg.formal_refused)}`);
|
|
39624
39650
|
return;
|
|
39625
39651
|
}
|
|
39626
|
-
const toolchain = findToolchain();
|
|
39627
39652
|
if (cfg.formal === "push") {
|
|
39628
|
-
if (
|
|
39653
|
+
if (findToolchain()) printInfo(" Formal verification at push \u2713");
|
|
39629
39654
|
else printWarn(" Formal verification is on but Lean is missing \u2014 run: verity lean install");
|
|
39630
39655
|
return;
|
|
39631
39656
|
}
|
|
39657
|
+
if (cfg.formal === "off") {
|
|
39658
|
+
printInfo(" Formal verification: off (enable with: verity config formal push)");
|
|
39659
|
+
return;
|
|
39660
|
+
}
|
|
39632
39661
|
if (defaultsOnly || !interactive()) {
|
|
39633
39662
|
printInfo(" Formal verification (a model-based bug finder at push): off \u2014 enable with: verity config formal push");
|
|
39634
39663
|
return;
|
|
39635
39664
|
}
|
|
39665
|
+
const toolchain = findToolchain();
|
|
39636
39666
|
console.log("");
|
|
39637
39667
|
console.log(" Formal verification review \u2014 a model-based bug finder for your riskiest code.");
|
|
39638
39668
|
console.log(" At push, Verity picks the riskiest changed function, checks a small model of");
|
|
@@ -39640,9 +39670,10 @@ async function offerFormalVerification(defaultsOnly, pluginMode) {
|
|
|
39640
39670
|
console.log(" Verity for that). Only a reproduced bug can block; most pushes get no model.");
|
|
39641
39671
|
console.log(` Adds /verity-prove <worry> to check any code on demand.${toolchain ? "" : ` Needs to install Lean 4 (${LEAN_DOWNLOAD_SIZE}) on "yes".`}`);
|
|
39642
39672
|
console.log("");
|
|
39643
|
-
const wants = await promptChoice("Formal verification at push?", FORMAL_CHOICES, "
|
|
39673
|
+
const wants = await promptChoice("Formal verification at push?", FORMAL_CHOICES, "no");
|
|
39644
39674
|
if (wants === "yes" && pluginMode) printInfo(" Plugin: the push check works now; /verity:verity-prove arrives with the plugin's next release.");
|
|
39645
39675
|
if (wants !== "yes") {
|
|
39676
|
+
writeProjectConfig({ formal: "off" });
|
|
39646
39677
|
printInfo(" Formal verification: off (enable later with: verity config formal push)");
|
|
39647
39678
|
return;
|
|
39648
39679
|
}
|
|
@@ -39901,7 +39932,7 @@ function registerInitCommand(program2) {
|
|
|
39901
39932
|
...telemetryChoice ? { telemetry: telemetryChoice } : {},
|
|
39902
39933
|
init: {
|
|
39903
39934
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39904
|
-
cli_version: true ? "0.34.0
|
|
39935
|
+
cli_version: true ? "0.34.0" : "dev"
|
|
39905
39936
|
}
|
|
39906
39937
|
});
|
|
39907
39938
|
} catch (err2) {
|
|
@@ -41374,8 +41405,8 @@ function registerLeanCommand(program2) {
|
|
|
41374
41405
|
}
|
|
41375
41406
|
|
|
41376
41407
|
// src/cli.ts
|
|
41377
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.34.0
|
|
41378
|
-
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.34.0
|
|
41408
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.34.0").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
|
|
41409
|
+
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.34.0");
|
|
41379
41410
|
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
41380
41411
|
try {
|
|
41381
41412
|
await foldLegacyLocalCredential();
|