@codacy/verity-cli 0.33.3-experimental.52f1656 → 0.33.3-experimental.86b5b6f
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/bin/verity.js +542 -92
- package/data/skills/verity-prove/SKILL.md +95 -134
- package/package.json +1 -1
package/bin/verity.js
CHANGED
|
@@ -22639,7 +22639,7 @@ function channelSilence(input) {
|
|
|
22639
22639
|
// src/lib/cli-version.ts
|
|
22640
22640
|
function cliVersion() {
|
|
22641
22641
|
try {
|
|
22642
|
-
return true ? "0.33.3-experimental.
|
|
22642
|
+
return true ? "0.33.3-experimental.86b5b6f" : "dev";
|
|
22643
22643
|
} catch {
|
|
22644
22644
|
return "dev";
|
|
22645
22645
|
}
|
|
@@ -26843,7 +26843,7 @@ var RISK_PATTERNS = [
|
|
|
26843
26843
|
var SOURCE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|kts|rb|cs|scala|swift|php|ex|exs)$/;
|
|
26844
26844
|
var EXCLUDED_DIR = /(^|\/)(node_modules|dist|build|bin|out|vendor|coverage|\.verity|\.claude|\.codacy|\.git|__pycache__|target|\.lake)\//;
|
|
26845
26845
|
var TEST_PATH2 = /(^|\/)(tests?|__tests__|spec|specs|fixtures?|e2e|testdata)\/|\.(test|spec|stories)\.[cm]?[jt]sx?$|_test\.(go|py|rb)$|test_[^/]*\.py$/;
|
|
26846
|
-
var GENERATED2 = /\.d\.ts$|\.min\.[jt]s$|\.generated\.|\.pb\.|lock\.json$|\.lock
|
|
26846
|
+
var GENERATED2 = /\.d\.ts$|\.min\.[jt]s$|\.generated\.|\.pb\.|lock\.json$|\.lock$|(^|\/)(integrations\/supabase\/types|database\.types|supabase\.types)\.ts$|(^|\/)__generated__\//;
|
|
26847
26847
|
function isCandidateSource(file) {
|
|
26848
26848
|
return SOURCE_EXT.test(file) && !EXCLUDED_DIR.test(file) && !TEST_PATH2.test(file) && !GENERATED2.test(file);
|
|
26849
26849
|
}
|
|
@@ -26982,11 +26982,37 @@ var deterministicRanker = {
|
|
|
26982
26982
|
return { ok: true, ranker: "deterministic", ranked: deterministicOrder(candidates2) };
|
|
26983
26983
|
}
|
|
26984
26984
|
};
|
|
26985
|
-
function excerpt(root, file, bytes, readSource) {
|
|
26985
|
+
function excerpt(root, file, bytes, readSource, changed) {
|
|
26986
26986
|
const read = readSource ? readSource(file, EXCERPT_SOURCE_CAP_BYTES) : readRegularFileInRoot(root, file, EXCERPT_SOURCE_CAP_BYTES);
|
|
26987
26987
|
if (!read.ok) return "";
|
|
26988
|
+
if (changed && changed.length > 0) return changedExcerpt(read.content, changed, bytes);
|
|
26988
26989
|
return read.content.length > bytes ? read.content.slice(0, bytes) + "\n\u2026 (truncated)" : read.content;
|
|
26989
26990
|
}
|
|
26991
|
+
function changedExcerpt(content, changed, bytes, context = 30) {
|
|
26992
|
+
const lines = content.split("\n");
|
|
26993
|
+
const windows = [...changed].sort((a, b) => a.from - b.from).map((r) => ({ from: Math.max(1, r.from - context), to: Math.min(lines.length, r.to + context) })).reduce((acc, w) => {
|
|
26994
|
+
const last = acc[acc.length - 1];
|
|
26995
|
+
if (last && w.from <= last.to + 1) last.to = Math.max(last.to, w.to);
|
|
26996
|
+
else acc.push({ ...w });
|
|
26997
|
+
return acc;
|
|
26998
|
+
}, []);
|
|
26999
|
+
const isChanged = (n) => changed.some((r) => n >= r.from && n <= r.to);
|
|
27000
|
+
const out = [];
|
|
27001
|
+
let size = 0;
|
|
27002
|
+
for (const w of windows) {
|
|
27003
|
+
if (out.length) out.push(" \u2026");
|
|
27004
|
+
for (let n = w.from; n <= w.to; n++) {
|
|
27005
|
+
const line = `${String(n).padStart(5)}${isChanged(n) ? "*" : " "}| ${lines[n - 1] ?? ""}`;
|
|
27006
|
+
size += line.length + 1;
|
|
27007
|
+
if (size > bytes) {
|
|
27008
|
+
out.push(" \u2026 (truncated)");
|
|
27009
|
+
return out.join("\n");
|
|
27010
|
+
}
|
|
27011
|
+
out.push(line);
|
|
27012
|
+
}
|
|
27013
|
+
}
|
|
27014
|
+
return out.join("\n");
|
|
27015
|
+
}
|
|
26990
27016
|
var EXCERPT_SOURCE_CAP_BYTES = 512 * 1024;
|
|
26991
27017
|
var LUNA_SCHEMA = {
|
|
26992
27018
|
type: "object",
|
|
@@ -27012,7 +27038,8 @@ var LUNA_SCHEMA = {
|
|
|
27012
27038
|
};
|
|
27013
27039
|
var LUNA_INSTRUCTIONS = `You select SMALL pieces of code worth building a formal model of (a Lean 4 transition system with bounded exploration). The goal is to find bugs, not to model the codebase.
|
|
27014
27040
|
For each file you are given its git-history signals and an excerpt. Score each 0-1 for "a model of one function here would likely expose a real bug": high for check-then-act guards, retries, caches, dedup keys, locks/leases, status transitions, money, concurrent writes; low for glue, rendering, configuration, or code whose correctness is obvious from reading.
|
|
27015
|
-
Name the single symbol (function) most worth modelling, and the applicable properties from the catalogue only. Reasons: at most 3, each a short concrete sentence about THIS code. Return every file you were given, exactly once
|
|
27041
|
+
Name the single symbol (function) most worth modelling, and the applicable properties from the catalogue only. Reasons: at most 3, each a short concrete sentence about THIS code. Return every file you were given, exactly once.
|
|
27042
|
+
When a file lists \`changed\` lines, this is a review of a push: the excerpt is numbered around the changes (changed lines starred). Name the function that CONTAINS a changed line, and score THAT FUNCTION'S CURRENT CODE as a whole \u2014 how likely a model of it exposes a real bug \u2014 never the edit itself. A one-line change (a log line, a rename, a comment) to a function that checks then acts, retries, dedups, moves money, grants access or changes a status still scores high: the push touched risky code, and a latent bug in it is exactly what this is for. Score low only when the changed function is itself glue, wiring, command registration, rendering, configuration or plainly correct.`;
|
|
27016
27043
|
var lunaRanker = {
|
|
27017
27044
|
name: "luna",
|
|
27018
27045
|
async rank(candidates2, ctx) {
|
|
@@ -27025,14 +27052,18 @@ var lunaRanker = {
|
|
|
27025
27052
|
if (!apiKey) return { ok: false, ranker: "luna", error: "OPENAI_API_KEY is not set", ranked: fallback };
|
|
27026
27053
|
const model = env.VERITY_PROVE_MODEL ?? "gpt-6-luna";
|
|
27027
27054
|
const excerptBytes = ctx.excerptBytes ?? 6 * 1024;
|
|
27028
|
-
const payload = candidates2.map((c) =>
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
|
|
27032
|
-
|
|
27033
|
-
|
|
27034
|
-
|
|
27035
|
-
|
|
27055
|
+
const payload = candidates2.map((c) => {
|
|
27056
|
+
const changed = ctx.changedLines?.get(c.file);
|
|
27057
|
+
return {
|
|
27058
|
+
file: c.file,
|
|
27059
|
+
commits: c.commits,
|
|
27060
|
+
fix_commits: c.fixCommits,
|
|
27061
|
+
risk: c.risk,
|
|
27062
|
+
lines: c.lines,
|
|
27063
|
+
excerpt: excerpt(ctx.root, c.file, excerptBytes, ctx.readSource, changed),
|
|
27064
|
+
...changed?.length ? { changed: changed.map((r) => r.from === r.to ? `${r.from}` : `${r.from}-${r.to}`).join(", ") } : {}
|
|
27065
|
+
};
|
|
27066
|
+
});
|
|
27036
27067
|
const body = {
|
|
27037
27068
|
model,
|
|
27038
27069
|
reasoning: { effort: "low" },
|
|
@@ -27087,7 +27118,8 @@ async function rankViaServer(candidates2, ctx, auth2) {
|
|
|
27087
27118
|
fix_commits: c.fixCommits,
|
|
27088
27119
|
risk: c.risk,
|
|
27089
27120
|
lines: c.lines,
|
|
27090
|
-
excerpt: excerpt(ctx.root, c.file, excerptBytes, ctx.readSource)
|
|
27121
|
+
excerpt: excerpt(ctx.root, c.file, excerptBytes, ctx.readSource, ctx.changedLines?.get(c.file)),
|
|
27122
|
+
...ctx.changedLines?.get(c.file)?.length ? { changed_lines: ctx.changedLines.get(c.file).slice(0, 50) } : {}
|
|
27091
27123
|
})),
|
|
27092
27124
|
...ctx.hint ? { hint: ctx.hint.slice(0, 500) } : {}
|
|
27093
27125
|
},
|
|
@@ -27389,6 +27421,46 @@ function lintModelSource(source) {
|
|
|
27389
27421
|
return reasons.length ? { ok: false, reasons: [...new Set(reasons)] } : { ok: true };
|
|
27390
27422
|
}
|
|
27391
27423
|
|
|
27424
|
+
// src/lib/prove/lean-format.ts
|
|
27425
|
+
function joinBrokenLiterals(source) {
|
|
27426
|
+
const raw = source.split("\n");
|
|
27427
|
+
const tok = tokenizeLean(source).code.split("\n");
|
|
27428
|
+
if (raw.length !== tok.length) return source;
|
|
27429
|
+
const outRaw = [];
|
|
27430
|
+
let depth = 0;
|
|
27431
|
+
let i = 0;
|
|
27432
|
+
while (i < raw.length) {
|
|
27433
|
+
let r = raw[i];
|
|
27434
|
+
let t = tok[i];
|
|
27435
|
+
i++;
|
|
27436
|
+
for (; ; ) {
|
|
27437
|
+
const d = depth + braceDelta(t);
|
|
27438
|
+
const end = t.trimEnd();
|
|
27439
|
+
if (!(d > 0 && end.endsWith(",") && i < raw.length)) break;
|
|
27440
|
+
while (i < raw.length && tok[i].trim() === "") i++;
|
|
27441
|
+
if (i >= raw.length) break;
|
|
27442
|
+
const cut = end.length;
|
|
27443
|
+
const nextRaw = raw[i];
|
|
27444
|
+
const nextTok = tok[i];
|
|
27445
|
+
const lead = nextTok.length - nextTok.trimStart().length;
|
|
27446
|
+
r = r.slice(0, cut) + " " + nextRaw.slice(lead);
|
|
27447
|
+
t = t.slice(0, cut) + " " + nextTok.slice(lead);
|
|
27448
|
+
i++;
|
|
27449
|
+
}
|
|
27450
|
+
depth = Math.max(0, depth + braceDelta(t));
|
|
27451
|
+
outRaw.push(r);
|
|
27452
|
+
}
|
|
27453
|
+
return outRaw.join("\n");
|
|
27454
|
+
}
|
|
27455
|
+
function braceDelta(code) {
|
|
27456
|
+
let d = 0;
|
|
27457
|
+
for (const ch of code) {
|
|
27458
|
+
if (ch === "{") d++;
|
|
27459
|
+
else if (ch === "}") d--;
|
|
27460
|
+
}
|
|
27461
|
+
return d;
|
|
27462
|
+
}
|
|
27463
|
+
|
|
27392
27464
|
// src/lib/prove/generate.ts
|
|
27393
27465
|
var LIBRARY_API = `The Verity Lean 4 library (already built; \`import Verity\` is the ONLY import allowed):
|
|
27394
27466
|
|
|
@@ -27460,6 +27532,8 @@ var RULES = `Write ONE Lean 4 model of the code below, in the style of the exemp
|
|
|
27460
27532
|
- 2 to 4 properties from this catalogue, all \`kind := .structural\`: idempotence, no lost update, status only moves forward, retries terminate, never fails open, bounded size, check-then-act is atomic, no double effect. Each \`inv\` must be a real claim the explorer can refute; no tautologies. Add an \`intent\` property ONLY if a comment in the source states the expectation, and cite its line in \`source\`.
|
|
27461
27533
|
- The header: \`abstracts := [{ file, symbol, sha256, lines }]\` with the exact values given below (lines "" when none).
|
|
27462
27534
|
- Exactly one \`#eval Verity.model\` and one \`#eval Verity.check\` per property, \`{ bound := 6 }\` or less. No other \`#eval\`, no \`IO\`, no \`sorry\`, no attributes, no macros, no \`partial\`. Under 120 lines.
|
|
27535
|
+
- Write every structure literal on ONE line: the \`abstracts\` entry, each \`{ s with \u2026 }\` update, each \`{ field := \u2026 }\` value. Lean reads a literal split across lines by its indentation and refuses it.
|
|
27536
|
+
- A property must be something the function PROMISES. Do not claim idempotence or no double effect for an operation whose job is to apply an effect on every call (a withdrawal, a charge, a send) unless the code dedups it (an idempotency key, a unique insert, an explicit check). When a trace needs a crash between two writes, say so in \`desc\` ("after a crash between X and Y, \u2026").
|
|
27463
27537
|
- Imports beyond \`Verity\` are refused by a linter; a file that does not elaborate gets ONE more attempt with the compiler's message.
|
|
27464
27538
|
Answer with JSON only: {"name": "<PascalCase>", "lean_source": "<the whole file>", "summary": "<one sentence: what the model abstracts and which property is most likely to fail>"}.`;
|
|
27465
27539
|
var SCHEMA = {
|
|
@@ -27484,7 +27558,7 @@ function renderGenerateInput(t, previous) {
|
|
|
27484
27558
|
parts.push("```");
|
|
27485
27559
|
if (previous) {
|
|
27486
27560
|
parts.push("");
|
|
27487
|
-
parts.push("# Your previous attempt
|
|
27561
|
+
parts.push("# Your previous attempt was not accepted \u2014 the diagnostics say why. Fix it (same model, same properties):");
|
|
27488
27562
|
parts.push("```lean");
|
|
27489
27563
|
parts.push(previous.source);
|
|
27490
27564
|
parts.push("```");
|
|
@@ -27497,7 +27571,7 @@ function renderGenerateInput(t, previous) {
|
|
|
27497
27571
|
}
|
|
27498
27572
|
function acceptAnswer(parsed, usage, costUsd = null) {
|
|
27499
27573
|
const name = typeof parsed.name === "string" && /^[A-Z][A-Za-z0-9]{1,40}$/.test(parsed.name) ? parsed.name : null;
|
|
27500
|
-
const source = typeof parsed.lean_source === "string" ? parsed.lean_source.replace(/\r\n/g, "\n") : null;
|
|
27574
|
+
const source = typeof parsed.lean_source === "string" ? joinBrokenLiterals(parsed.lean_source.replace(/\r\n/g, "\n")) : null;
|
|
27501
27575
|
if (!name || !source) return { ok: false, error: "answer lacked a valid name or lean_source", usage, costUsd };
|
|
27502
27576
|
const lint = lintModelSource(source);
|
|
27503
27577
|
if (!lint.ok) return { ok: false, error: `linter refused the model: ${lint.reasons.join("; ")}`, usage, costUsd };
|
|
@@ -27510,12 +27584,15 @@ async function generateModel(target, opts) {
|
|
|
27510
27584
|
path: "/prove/generate",
|
|
27511
27585
|
serviceUrl: opts.auth.serviceUrl,
|
|
27512
27586
|
token: opts.auth.token,
|
|
27513
|
-
|
|
27587
|
+
// Streamed with a heartbeat so a long generation cannot die idle (decision 53).
|
|
27588
|
+
extraHeaders: { "X-Verity-Keepalive": "1" },
|
|
27589
|
+
body: { target, tier: opts.tier ?? "deep", ...opts.previous ? { previous: opts.previous } : {}, ...opts.budgetMs ? { budget_ms: Math.max(2e4, Math.round(opts.budgetMs)) } : {} },
|
|
27514
27590
|
timeout: opts.budgetMs ? Math.max(2e4, Math.round(opts.budgetMs)) + 15e3 : opts.timeoutMs ?? 13e4,
|
|
27515
27591
|
cmd: "prove-generate"
|
|
27516
27592
|
});
|
|
27517
27593
|
if (!res.ok) return { ok: false, error: res.error };
|
|
27518
27594
|
const d = res.data ?? {};
|
|
27595
|
+
if (d.error && typeof d.error === "object") return { ok: false, error: `${d.error.code ?? "SERVICE_ERROR"}: ${d.error.message ?? "the service failed"}` };
|
|
27519
27596
|
const usage2 = d.usage && typeof d.usage === "object" ? { inputTokens: Number(d.usage.inputTokens ?? 0), outputTokens: Number(d.usage.outputTokens ?? 0) } : null;
|
|
27520
27597
|
return acceptAnswer(d, usage2, typeof d.cost_usd === "number" ? d.cost_usd : null);
|
|
27521
27598
|
}
|
|
@@ -27525,7 +27602,7 @@ async function generateModel(target, opts) {
|
|
|
27525
27602
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
27526
27603
|
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
27527
27604
|
const body = {
|
|
27528
|
-
model: opts.model ?? "gpt-5.6-terra",
|
|
27605
|
+
model: opts.model ?? (opts.tier === "fast" ? "gpt-6-luna" : "gpt-5.6-terra"),
|
|
27529
27606
|
instructions: `${RULES}
|
|
27530
27607
|
|
|
27531
27608
|
${LIBRARY_API}
|
|
@@ -27535,7 +27612,7 @@ ${LIBRARY_API}
|
|
|
27535
27612
|
${EXEMPLAR}
|
|
27536
27613
|
\`\`\``,
|
|
27537
27614
|
input: [{ role: "user", content: renderGenerateInput(target, opts.previous) }],
|
|
27538
|
-
reasoning: { effort: "medium" },
|
|
27615
|
+
reasoning: { effort: opts.tier === "fast" ? "low" : "medium" },
|
|
27539
27616
|
max_output_tokens: 12e3,
|
|
27540
27617
|
store: false,
|
|
27541
27618
|
text: { format: { type: "json_schema", name: "lean_model", schema: SCHEMA, strict: true } }
|
|
@@ -27963,6 +28040,8 @@ async function runReplayViaServer(brief, auth2, budgetMs = 24e4) {
|
|
|
27963
28040
|
budget_ms: budget
|
|
27964
28041
|
},
|
|
27965
28042
|
timeout: budget + 15e3,
|
|
28043
|
+
// Streamed with a heartbeat so a long replay cannot die idle (decision 53).
|
|
28044
|
+
extraHeaders: { "X-Verity-Keepalive": "1" },
|
|
27966
28045
|
cmd: "prove-replay"
|
|
27967
28046
|
});
|
|
27968
28047
|
const none = { covered: 0, total: 0, ratio: 0, basis: "none" };
|
|
@@ -27970,6 +28049,11 @@ async function runReplayViaServer(brief, auth2, budgetMs = 24e4) {
|
|
|
27970
28049
|
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 };
|
|
27971
28050
|
}
|
|
27972
28051
|
const d = res.data ?? {};
|
|
28052
|
+
if (d.error && typeof d.error === "object") {
|
|
28053
|
+
const e = d.error;
|
|
28054
|
+
const msg = `${e.code ?? "SERVICE_ERROR"}: ${e.message ?? "the service failed"}`;
|
|
28055
|
+
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 };
|
|
28056
|
+
}
|
|
27973
28057
|
const verdict = d.verdict;
|
|
27974
28058
|
const v = typeof d.verbatim === "object" && d.verbatim ? d.verbatim : none;
|
|
27975
28059
|
return {
|
|
@@ -28929,7 +29013,7 @@ function writeResult(root, report) {
|
|
|
28929
29013
|
|
|
28930
29014
|
// src/lib/prove/auto.ts
|
|
28931
29015
|
var AUTO_MODELS_DIR = `${MODELS_DIR}/.auto`;
|
|
28932
|
-
var DEFAULT_MIN_SCORE = 0.
|
|
29016
|
+
var DEFAULT_MIN_SCORE = 0.75;
|
|
28933
29017
|
var LARGE_PUSH_FILES = 25;
|
|
28934
29018
|
var DAILY_MODEL_CAP = 5;
|
|
28935
29019
|
var PUSH_COST_CAP_USD = 1;
|
|
@@ -29007,7 +29091,11 @@ function bumpLedger(root) {
|
|
|
29007
29091
|
const l = readLedger(root);
|
|
29008
29092
|
writeLedger(root, { ...l, models: l.models + 1 });
|
|
29009
29093
|
}
|
|
29010
|
-
var REPLAY_RESERVE_MS =
|
|
29094
|
+
var REPLAY_RESERVE_MS = 45e3;
|
|
29095
|
+
var MAX_REPLAYS = 3;
|
|
29096
|
+
function isNetworkError(message) {
|
|
29097
|
+
return /Network error|fetch failed|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|socket hang up/i.test(message);
|
|
29098
|
+
}
|
|
29011
29099
|
var GENERATE_RESERVE_MS = 4e4;
|
|
29012
29100
|
function workingTreeReader(root) {
|
|
29013
29101
|
return (file, maxBytes) => {
|
|
@@ -29021,8 +29109,9 @@ function sha256Of(content) {
|
|
|
29021
29109
|
function defaultDeps(opts) {
|
|
29022
29110
|
const env = opts.env ?? process.env;
|
|
29023
29111
|
return {
|
|
29024
|
-
rank: (candidates2, root) => lunaRanker.rank(candidates2, { root, auth: opts.auth, env, fetchImpl: opts.fetchImpl, readSource: opts.readSource }),
|
|
29025
|
-
|
|
29112
|
+
rank: (candidates2, root, changedLines) => lunaRanker.rank(candidates2, { root, auth: opts.auth, env, fetchImpl: opts.fetchImpl, readSource: opts.readSource, ...changedLines ? { changedLines } : {} }),
|
|
29113
|
+
// The push phase writes with the FAST tier; `prove generate` asks for DEEP (decision 56).
|
|
29114
|
+
generate: (target, previous, budgetMs) => generateModel(target, { auth: opts.auth, apiKey: opts.apiKey, env, fetchImpl: opts.fetchImpl, previous, tier: opts.tier ?? "fast", ...budgetMs ? { budgetMs } : {} }),
|
|
29026
29115
|
check: (root, modelFile) => checkModelAsync(opts.toolchain, root, modelFile),
|
|
29027
29116
|
replay: (root, report, property, modelSource, budgetMs) => runReplay(buildReplayBrief(root, report, property, modelSource, opts.readSource), { auth: opts.auth, apiKey: opts.apiKey, env, fetchImpl: opts.fetchImpl, maxRounds: 2, ...budgetMs ? { timeoutMs: budgetMs } : {} }),
|
|
29028
29117
|
now: () => Date.now(),
|
|
@@ -29107,33 +29196,56 @@ async function runFormalPhase(input) {
|
|
|
29107
29196
|
const best = signals[0];
|
|
29108
29197
|
return done({ status: "skipped", skip: "not-eligible", detail: `${best.file}: ${isEligible(best, largePush).why}` });
|
|
29109
29198
|
}
|
|
29110
|
-
const ranked = await deps.rank(eligible, root);
|
|
29199
|
+
const ranked = await deps.rank(eligible, root, input.changedLines);
|
|
29111
29200
|
if (ranked.ok) spend(ranked);
|
|
29112
29201
|
if (cancelled()) return done({ status: "skipped", skip: "deadline", detail: "cut off after ranking" });
|
|
29113
|
-
const
|
|
29114
|
-
|
|
29115
|
-
if (
|
|
29116
|
-
|
|
29117
|
-
|
|
29118
|
-
const
|
|
29119
|
-
|
|
29120
|
-
|
|
29121
|
-
|
|
29122
|
-
|
|
29123
|
-
|
|
29124
|
-
|
|
29125
|
-
|
|
29126
|
-
|
|
29127
|
-
|
|
29202
|
+
const minScore = input.minScore ?? DEFAULT_MIN_SCORE;
|
|
29203
|
+
const qualified = ranked.ranked.filter((c) => c.ai && c.ai.score >= minScore);
|
|
29204
|
+
if (qualified.length === 0) return done({ status: "skipped", skip: "no-candidate", detail: ranked.ok ? `best score ${Math.round((ranked.ranked[0]?.ai?.score ?? 0) * 100)}% of ${eligible.length} eligible` : `rank unavailable: ${ranked.error ?? ""}` });
|
|
29205
|
+
let chosen = null;
|
|
29206
|
+
let why = null;
|
|
29207
|
+
const note = (skip, detail) => {
|
|
29208
|
+
if (!why) why = { skip, detail };
|
|
29209
|
+
};
|
|
29210
|
+
for (const c of qualified) {
|
|
29211
|
+
if (!c.ai?.symbol) {
|
|
29212
|
+
note("no-symbol", c.file);
|
|
29213
|
+
continue;
|
|
29214
|
+
}
|
|
29215
|
+
const read = readSrc(c.file, 96 * 1024);
|
|
29216
|
+
if (!read.ok) {
|
|
29217
|
+
note("read-failed", `${c.file}: ${read.reason}`);
|
|
29218
|
+
continue;
|
|
29219
|
+
}
|
|
29220
|
+
const d = describeTarget(read.content, c.ai.symbol, c.file);
|
|
29221
|
+
if (input.changedLines) {
|
|
29222
|
+
const changed = input.changedLines.get(c.file) ?? [];
|
|
29223
|
+
if (changed.length === 0) {
|
|
29224
|
+
note("symbol-not-in-push", `${c.file}: no changed lines in this push`);
|
|
29225
|
+
continue;
|
|
29226
|
+
}
|
|
29227
|
+
if (!d.lines) {
|
|
29228
|
+
note("symbol-not-in-push", `${c.ai.symbol}: body not found, cannot tell whether the push touched it`);
|
|
29229
|
+
continue;
|
|
29230
|
+
}
|
|
29231
|
+
if (!intersects(d.lines, changed)) {
|
|
29232
|
+
note("symbol-not-in-push", `${c.ai.symbol} (lines ${d.lines.from}\u2013${d.lines.to}) unchanged by this push`);
|
|
29233
|
+
continue;
|
|
29234
|
+
}
|
|
29235
|
+
}
|
|
29236
|
+
chosen = { top: c, content: read.content, lines: d.lines, numbered: d.numbered };
|
|
29237
|
+
break;
|
|
29128
29238
|
}
|
|
29239
|
+
if (!chosen || !chosen.top.ai) {
|
|
29240
|
+
const first = qualified[0];
|
|
29241
|
+
const t0 = first.ai?.symbol ? { file: first.file, symbol: first.ai.symbol, score: first.ai.score, reasons: [...first.reasons, ...first.ai.reasons] } : void 0;
|
|
29242
|
+
return done({ status: "skipped", skip: why?.skip ?? "no-symbol", detail: why?.detail ?? first.file, ...t0 ? { target: t0 } : {} });
|
|
29243
|
+
}
|
|
29244
|
+
const top = chosen.top;
|
|
29245
|
+
const sha256 = sha256Of(chosen.content);
|
|
29246
|
+
const { lines, numbered } = chosen;
|
|
29129
29247
|
const target = { file: top.file, symbol: top.ai.symbol, lines, score: top.ai.score, reasons: [...top.reasons, ...top.ai.reasons] };
|
|
29130
29248
|
progress.target = target;
|
|
29131
|
-
if (input.changedLines) {
|
|
29132
|
-
const changed = input.changedLines.get(top.file) ?? [];
|
|
29133
|
-
if (changed.length === 0) return done({ status: "skipped", skip: "symbol-not-in-push", detail: `${top.file}: no changed lines in this push`, target });
|
|
29134
|
-
if (!lines) return done({ status: "skipped", skip: "symbol-not-in-push", detail: `${top.ai.symbol}: body not found, cannot tell whether the push touched it`, target });
|
|
29135
|
-
if (!intersects(lines, changed)) return done({ status: "skipped", skip: "symbol-not-in-push", detail: `${top.ai.symbol} (lines ${lines.from}\u2013${lines.to}) unchanged by this push`, target });
|
|
29136
|
-
}
|
|
29137
29249
|
if (timeLeft() < GENERATE_RESERVE_MS) return done({ status: "skipped", skip: "not-enough-time", detail: `${Math.round(timeLeft() / 1e3)} s left before generation`, target });
|
|
29138
29250
|
bumpLedger(root);
|
|
29139
29251
|
const genTarget = { file: top.file, symbol: top.ai.symbol, lines, sha256, numbered, reasons: target.reasons, properties: top.ai.properties };
|
|
@@ -29150,8 +29262,11 @@ async function runFormalPhase(input) {
|
|
|
29150
29262
|
spend(gen);
|
|
29151
29263
|
if (cancelled()) return done({ status: "skipped", skip: "deadline", detail: "cut off during generation", target });
|
|
29152
29264
|
if (!gen.ok) {
|
|
29153
|
-
|
|
29154
|
-
|
|
29265
|
+
const network = isNetworkError(gen.error);
|
|
29266
|
+
if (round === 2 || !(network || /linter refused/.test(gen.error)) || timeLeft() < GENERATE_RESERVE_MS) {
|
|
29267
|
+
return done({ status: "skipped", skip: "generation-failed", detail: gen.error, target });
|
|
29268
|
+
}
|
|
29269
|
+
if (!network) previous = { source: "", error: gen.error };
|
|
29155
29270
|
continue;
|
|
29156
29271
|
}
|
|
29157
29272
|
modelName = gen.name;
|
|
@@ -29184,27 +29299,51 @@ async function runFormalPhase(input) {
|
|
|
29184
29299
|
writeResult(root, report);
|
|
29185
29300
|
} catch {
|
|
29186
29301
|
}
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
|
|
29302
|
+
const cexes = report.properties.filter((p) => p.result === "counterexample");
|
|
29303
|
+
const baseOf = (p) => ({ property: p.id, desc: p.desc, kind: p.kind, trace: p.trace ?? [], file: top.file, symbol: top.ai.symbol });
|
|
29304
|
+
const maxReplays = input.maxReplays ?? MAX_REPLAYS;
|
|
29305
|
+
let skipWhy = null;
|
|
29306
|
+
if (timeLeft() < REPLAY_RESERVE_MS) skipWhy = `not replayed: ${Math.round(timeLeft() / 1e3)} s left`;
|
|
29307
|
+
else if (overCap()) skipWhy = `not replayed: $${usage.costUsd.toFixed(2)} spent, cap $${costCap.toFixed(2)}`;
|
|
29308
|
+
else if (cancelled()) skipWhy = "not replayed: the phase was cut off";
|
|
29309
|
+
const toReplay = skipWhy ? [] : cexes.slice(0, maxReplays);
|
|
29310
|
+
const budget = Math.max(2e4, timeLeft() - 5e3);
|
|
29311
|
+
const byId = /* @__PURE__ */ new Map();
|
|
29312
|
+
let stoppedEarly = false;
|
|
29313
|
+
await new Promise((resolve8) => {
|
|
29314
|
+
let left = toReplay.length;
|
|
29315
|
+
if (left === 0) return resolve8();
|
|
29316
|
+
const settle = () => {
|
|
29317
|
+
if (--left === 0) resolve8();
|
|
29318
|
+
};
|
|
29319
|
+
for (const p of toReplay) {
|
|
29320
|
+
void (async () => {
|
|
29321
|
+
if (cancelled()) {
|
|
29322
|
+
byId.set(p.id, null);
|
|
29323
|
+
return;
|
|
29324
|
+
}
|
|
29325
|
+
const r = await deps.replay(root, report, p, modelSource.ok ? modelSource.content : "", budget);
|
|
29326
|
+
spend(r);
|
|
29327
|
+
if (!stoppedEarly) byId.set(p.id, r);
|
|
29328
|
+
if (r.verdict === "reproduced" && !stoppedEarly) {
|
|
29329
|
+
stoppedEarly = true;
|
|
29330
|
+
resolve8();
|
|
29331
|
+
}
|
|
29332
|
+
})().catch(() => {
|
|
29333
|
+
byId.set(p.id, null);
|
|
29334
|
+
}).finally(settle);
|
|
29198
29335
|
}
|
|
29199
|
-
|
|
29200
|
-
|
|
29336
|
+
});
|
|
29337
|
+
const replayedIds = new Set(toReplay.map((p) => p.id));
|
|
29338
|
+
for (const p of cexes) {
|
|
29339
|
+
const r = byId.get(p.id);
|
|
29340
|
+
if (!r) {
|
|
29341
|
+
const reason = skipWhy ?? (replayedIds.has(p.id) && byId.has(p.id) ? "not replayed: the phase was cut off" : replayedIds.has(p.id) ? "not waited for: another counterexample already reproduced" : `not replayed: ${maxReplays} per push`);
|
|
29342
|
+
findings.push({ ...baseOf(p), verdict: "model-only", reason });
|
|
29201
29343
|
continue;
|
|
29202
29344
|
}
|
|
29203
|
-
replays++;
|
|
29204
|
-
const r = await deps.replay(root, report, p, modelSource.ok ? modelSource.content : "", Math.max(2e4, timeLeft() - 5e3));
|
|
29205
|
-
spend(r);
|
|
29206
29345
|
const verdict = r.verdict === "reproduced" ? "reproduced" : r.verdict === "diverged" ? "diverged" : r.verdict === "unreplayable" ? "model-only" : "unverified";
|
|
29207
|
-
findings.push({ ...
|
|
29346
|
+
findings.push({ ...baseOf(p), verdict, reason: r.reason || r.downgrade || void 0 });
|
|
29208
29347
|
}
|
|
29209
29348
|
const reproduced = findings.filter((f) => f.verdict === "reproduced");
|
|
29210
29349
|
if (reproduced.length > 0 && !cancelled()) {
|
|
@@ -29223,6 +29362,63 @@ async function runFormalPhase(input) {
|
|
|
29223
29362
|
findings
|
|
29224
29363
|
});
|
|
29225
29364
|
}
|
|
29365
|
+
function describeTarget(content, symbol, file, range) {
|
|
29366
|
+
const all = content.split("\n");
|
|
29367
|
+
if (range) {
|
|
29368
|
+
const from2 = Math.max(1, range.from);
|
|
29369
|
+
const to = Math.min(all.length, range.to);
|
|
29370
|
+
return { lines: { from: from2, to }, numbered: numberLines(all.slice(from2 - 1, to).join("\n"), from2) };
|
|
29371
|
+
}
|
|
29372
|
+
const body = extractSymbolBody(content, symbol, file);
|
|
29373
|
+
if (!body) return { numbered: numberLines(content.slice(0, 24 * 1024)) };
|
|
29374
|
+
const n = body.split("\n").length;
|
|
29375
|
+
const from = all.findIndex((_, i) => all.slice(i, i + n).join("\n") === body) + 1;
|
|
29376
|
+
return { lines: from > 0 ? { from, to: from + n - 1 } : void 0, numbered: numberLines(body, from > 0 ? from : 1) };
|
|
29377
|
+
}
|
|
29378
|
+
async function generateAndCheck(root, req, opts) {
|
|
29379
|
+
const readSrc = opts.deps.readSource ?? workingTreeReader(root);
|
|
29380
|
+
const read = readSrc(req.file, 96 * 1024);
|
|
29381
|
+
if (!read.ok) return { ok: false, error: `${req.file}: ${read.reason}`, rounds: 0, costUsd: 0 };
|
|
29382
|
+
const sha256 = sha256Of(read.content);
|
|
29383
|
+
const { lines, numbered } = describeTarget(read.content, req.symbol, req.file, req.lines);
|
|
29384
|
+
const target = {
|
|
29385
|
+
file: req.file,
|
|
29386
|
+
symbol: req.symbol,
|
|
29387
|
+
lines,
|
|
29388
|
+
sha256,
|
|
29389
|
+
numbered,
|
|
29390
|
+
reasons: req.hint ? [`the user's worry, in their words: ${req.hint.slice(0, 280)}`] : [],
|
|
29391
|
+
properties: []
|
|
29392
|
+
};
|
|
29393
|
+
const dir = opts.modelsDir ?? MODELS_DIR;
|
|
29394
|
+
const maxRounds = opts.maxRounds ?? 3;
|
|
29395
|
+
let previous = req.revise ? { source: req.revise.source, error: `The model elaborated, but its counterexample DIVERGED when replayed on the real code: ${req.revise.note.slice(0, 2e3)}. Revise the model so it matches what the code does; keep the same properties.` } : void 0;
|
|
29396
|
+
let costUsd = 0;
|
|
29397
|
+
let lastError = "no attempt";
|
|
29398
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
29399
|
+
const gen = await opts.deps.generate(target, previous);
|
|
29400
|
+
if (typeof gen.costUsd === "number") costUsd += gen.costUsd;
|
|
29401
|
+
if (!gen.ok) {
|
|
29402
|
+
lastError = gen.error;
|
|
29403
|
+
const network = isNetworkError(gen.error);
|
|
29404
|
+
if (!network && !/linter refused/.test(gen.error)) return { ok: false, error: gen.error, rounds: round, costUsd };
|
|
29405
|
+
if (!network) previous = { source: "", error: gen.error };
|
|
29406
|
+
continue;
|
|
29407
|
+
}
|
|
29408
|
+
const rel = `${dir}/${gen.name}.lean`;
|
|
29409
|
+
writeFileInRootNoFollow(root, rel, gen.source);
|
|
29410
|
+
const abs = resolveModelFile(root, rel) ?? (0, import_node_path41.join)(root, rel);
|
|
29411
|
+
const report = await opts.deps.check(root, abs);
|
|
29412
|
+
const abstracts = report.header?.abstracts ?? [];
|
|
29413
|
+
const stray = abstracts.find((a) => a.file !== req.file);
|
|
29414
|
+
if (report.ok && report.properties.length > 0 && abstracts.length > 0 && !stray) {
|
|
29415
|
+
return { ok: true, name: gen.name, file: rel, report, rounds: round, costUsd };
|
|
29416
|
+
}
|
|
29417
|
+
lastError = stray ? `the model abstracts ${stray.file}, not ${req.file}` : report.error ?? (report.malformed ? "the model printed malformed VERITY lines" : "the model reported no property");
|
|
29418
|
+
previous = { source: gen.source, error: lastError };
|
|
29419
|
+
}
|
|
29420
|
+
return { ok: false, error: `no model of ${req.symbol} elaborated in ${maxRounds} round(s): ${lastError.slice(0, 300)}`, rounds: maxRounds, costUsd };
|
|
29421
|
+
}
|
|
29226
29422
|
function skippedOutcome(skip, detail) {
|
|
29227
29423
|
return { status: "skipped", skip, ...detail ? { detail } : {}, findings: [], usage: { inputTokens: 0, outputTokens: 0, costUsd: 0, unpriced: 0 }, durationMs: 0 };
|
|
29228
29424
|
}
|
|
@@ -29283,6 +29479,9 @@ function describeFormalOutcome(o) {
|
|
|
29283
29479
|
}
|
|
29284
29480
|
|
|
29285
29481
|
// src/lib/prove/merge.ts
|
|
29482
|
+
function isCrashTrace(trace) {
|
|
29483
|
+
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));
|
|
29484
|
+
}
|
|
29286
29485
|
function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
29287
29486
|
if (!outcome) return { blocking: 0 };
|
|
29288
29487
|
const norm = (p) => p.replace(/\\/g, "/");
|
|
@@ -29298,7 +29497,11 @@ function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
|
29298
29497
|
scope: "change",
|
|
29299
29498
|
file: f.file,
|
|
29300
29499
|
line: outcome.target?.lines?.from ?? null,
|
|
29301
|
-
|
|
29500
|
+
// Worded as the VIOLATION: the property text is what should hold, and
|
|
29501
|
+
// as a title it read backwards (e2e, 2026-09-26). A trace that needs a
|
|
29502
|
+
// crash between two writes is labelled: reproducing it means injecting
|
|
29503
|
+
// the crash, so it is a crash-consistency finding, not a race.
|
|
29504
|
+
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`,
|
|
29302
29505
|
description: terminalText(f.reason ?? "", 2e3),
|
|
29303
29506
|
fix: { description: `Trace: ${terminalText(f.trace.join(" \u2192 ") || "(initial state)", 2e3)}. Replay: ${terminalText(f.reason ?? "reproduced in the container", 2e3)}` },
|
|
29304
29507
|
source: "formal"
|
|
@@ -31036,13 +31239,6 @@ async function promptYes(question, opts) {
|
|
|
31036
31239
|
const a = answer.toLowerCase();
|
|
31037
31240
|
return a === "" || a === "y" || a === "yes";
|
|
31038
31241
|
}
|
|
31039
|
-
async function promptYesDefaultNo(question) {
|
|
31040
|
-
if (!interactive()) return false;
|
|
31041
|
-
const answer = await ask(question);
|
|
31042
|
-
if (answer === null) return false;
|
|
31043
|
-
const a = answer.toLowerCase();
|
|
31044
|
-
return a === "y" || a === "yes";
|
|
31045
|
-
}
|
|
31046
31242
|
function printOptions(question, choices) {
|
|
31047
31243
|
console.log("");
|
|
31048
31244
|
console.log(` ${question}`);
|
|
@@ -31162,6 +31358,32 @@ async function download(url, dest, onProgress) {
|
|
|
31162
31358
|
});
|
|
31163
31359
|
await (0, import_promises18.pipeline)(stream, (0, import_node_fs60.createWriteStream)(dest));
|
|
31164
31360
|
}
|
|
31361
|
+
function inPlaceProgress(label2, write = (s) => process.stderr.write(s)) {
|
|
31362
|
+
let active = false;
|
|
31363
|
+
let lastAt = 0;
|
|
31364
|
+
let lastPct = -1;
|
|
31365
|
+
const mb = (n) => (n / 1048576).toFixed(0);
|
|
31366
|
+
return {
|
|
31367
|
+
update(seen, total) {
|
|
31368
|
+
const now = Date.now();
|
|
31369
|
+
const pct = total ? Math.min(100, Math.floor(seen / total * 100)) : -1;
|
|
31370
|
+
if (now - lastAt < 100 && pct !== 100 && pct === lastPct) return;
|
|
31371
|
+
lastAt = now;
|
|
31372
|
+
lastPct = pct;
|
|
31373
|
+
active = true;
|
|
31374
|
+
const width = 24;
|
|
31375
|
+
const filled = pct >= 0 ? Math.round(pct / 100 * width) : 0;
|
|
31376
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
31377
|
+
const tail = total ? `${String(pct).padStart(3)}% ${mb(seen)}/${mb(total)} MB` : `${mb(seen)} MB`;
|
|
31378
|
+
write(`\r\x1B[2K${GREEN}[verity]${NC} ${label2} ${bar} ${tail}`);
|
|
31379
|
+
},
|
|
31380
|
+
done() {
|
|
31381
|
+
if (!active) return;
|
|
31382
|
+
active = false;
|
|
31383
|
+
write("\n");
|
|
31384
|
+
}
|
|
31385
|
+
};
|
|
31386
|
+
}
|
|
31165
31387
|
async function installLean(opts = {}) {
|
|
31166
31388
|
const say = opts.onStatus ?? (() => {
|
|
31167
31389
|
});
|
|
@@ -31180,6 +31402,7 @@ async function installLean(opts = {}) {
|
|
|
31180
31402
|
let lastPct = -1;
|
|
31181
31403
|
try {
|
|
31182
31404
|
await download(url, zip, (seen, total) => {
|
|
31405
|
+
if (opts.onProgress) return opts.onProgress(seen, total);
|
|
31183
31406
|
const pct = total ? Math.floor(seen / total * 100) : -1;
|
|
31184
31407
|
if (pct !== lastPct && pct % 10 === 0) {
|
|
31185
31408
|
lastPct = pct;
|
|
@@ -31478,6 +31701,10 @@ var INTENSITY_CHOICES = [
|
|
|
31478
31701
|
{ id: "balanced", label: "balanced", hint: "security + quality (~8s)", recommended: true },
|
|
31479
31702
|
{ id: "thorough", label: "thorough", hint: "all tools, all rules (~15s)" }
|
|
31480
31703
|
];
|
|
31704
|
+
var FORMAL_CHOICES = [
|
|
31705
|
+
{ id: "yes", label: "yes", hint: "a Lean model of the riskiest changed function; blocks only on a REPRODUCED counterexample", recommended: true },
|
|
31706
|
+
{ id: "no", label: "no", hint: "enable later with: verity config formal push" }
|
|
31707
|
+
];
|
|
31481
31708
|
var MOMENT_CHOICES = [
|
|
31482
31709
|
{ id: "stop", label: "On stop", hint: "after every agent turn \u2014 fast feedback while you work", recommended: true },
|
|
31483
31710
|
{ id: "pre-commit", label: "Before commit", hint: "reviews the staged diff, blocks the commit on FAIL" },
|
|
@@ -31804,7 +32031,7 @@ async function checkPrerequisites(step) {
|
|
|
31804
32031
|
}
|
|
31805
32032
|
return prereqs.checks.some((c) => c.id === "claude" && c.status === "ok");
|
|
31806
32033
|
}
|
|
31807
|
-
async function scaffoldProject(step, defaultsOnly) {
|
|
32034
|
+
async function scaffoldProject(step, defaultsOnly, pluginMode) {
|
|
31808
32035
|
step("Knowledge base, .gitignore and CLAUDE.md");
|
|
31809
32036
|
await (0, import_promises20.mkdir)(VERITY_DIR, { recursive: true });
|
|
31810
32037
|
await ensureMemoryDir();
|
|
@@ -31859,9 +32086,9 @@ async function scaffoldProject(step, defaultsOnly) {
|
|
|
31859
32086
|
} catch (err) {
|
|
31860
32087
|
printWarn(` Could not update CLAUDE.md: ${err.message}`);
|
|
31861
32088
|
}
|
|
31862
|
-
await offerFormalVerification(defaultsOnly);
|
|
32089
|
+
await offerFormalVerification(defaultsOnly, pluginMode);
|
|
31863
32090
|
}
|
|
31864
|
-
async function offerFormalVerification(defaultsOnly) {
|
|
32091
|
+
async function offerFormalVerification(defaultsOnly, pluginMode) {
|
|
31865
32092
|
const cfg = readProjectConfig();
|
|
31866
32093
|
const toolchain = findToolchain();
|
|
31867
32094
|
if (cfg.formal === "push") {
|
|
@@ -31869,19 +32096,25 @@ async function offerFormalVerification(defaultsOnly) {
|
|
|
31869
32096
|
else printWarn(" Formal verification is on but Lean is missing \u2014 run: verity lean install");
|
|
31870
32097
|
return;
|
|
31871
32098
|
}
|
|
31872
|
-
if (defaultsOnly) {
|
|
31873
|
-
printInfo(" Formal verification (
|
|
32099
|
+
if (defaultsOnly || !interactive()) {
|
|
32100
|
+
printInfo(" Formal verification (a model-based bug finder at push): off \u2014 enable with: verity config formal push");
|
|
31874
32101
|
return;
|
|
31875
32102
|
}
|
|
31876
|
-
|
|
31877
|
-
|
|
31878
|
-
);
|
|
31879
|
-
|
|
32103
|
+
console.log("");
|
|
32104
|
+
console.log(" Formal verification review \u2014 a model-based bug finder for your riskiest code.");
|
|
32105
|
+
console.log(" At push, Verity picks the riskiest changed function, checks a small model of");
|
|
32106
|
+
console.log(" it, and replays any counterexample on the real code (its source is sent to");
|
|
32107
|
+
console.log(" Verity for that). Only a reproduced bug can block; most pushes get no model.");
|
|
32108
|
+
console.log(` Adds /verity-prove <worry> to check any code on demand.${toolchain ? "" : ` Needs to install Lean 4 (${LEAN_DOWNLOAD_SIZE}) on "yes".`}`);
|
|
32109
|
+
console.log("");
|
|
32110
|
+
const wants = await promptChoice("Formal verification at push?", FORMAL_CHOICES, "yes");
|
|
32111
|
+
if (wants === "yes" && pluginMode) printInfo(" Plugin: the push check works now; /verity:verity-prove arrives with the plugin's next release.");
|
|
32112
|
+
if (wants !== "yes") {
|
|
31880
32113
|
printInfo(" Formal verification: off (enable later with: verity config formal push)");
|
|
31881
32114
|
return;
|
|
31882
32115
|
}
|
|
31883
32116
|
if (!toolchain) {
|
|
31884
|
-
const result = await
|
|
32117
|
+
const result = await installLeanWithProgress();
|
|
31885
32118
|
if (!result.ok) {
|
|
31886
32119
|
printWarn(` Lean install failed: ${result.error} \u2014 formal verification left off; retry with: verity lean install`);
|
|
31887
32120
|
return;
|
|
@@ -31891,6 +32124,18 @@ async function offerFormalVerification(defaultsOnly) {
|
|
|
31891
32124
|
writeProjectConfig({ formal: "push" });
|
|
31892
32125
|
printInfo(" Formal verification at push \u2713 (this checkout; `verity config formal off` turns it off)");
|
|
31893
32126
|
}
|
|
32127
|
+
async function installLeanWithProgress() {
|
|
32128
|
+
const progress = process.stderr.isTTY ? inPlaceProgress(`downloading ${leanAssetName() ?? "Lean 4"}`) : null;
|
|
32129
|
+
const result = await installLean({
|
|
32130
|
+
onStatus: (line) => {
|
|
32131
|
+
progress?.done();
|
|
32132
|
+
printInfo(` ${line}`);
|
|
32133
|
+
},
|
|
32134
|
+
...progress ? { onProgress: progress.update } : {}
|
|
32135
|
+
});
|
|
32136
|
+
progress?.done();
|
|
32137
|
+
return result;
|
|
32138
|
+
}
|
|
31894
32139
|
function registerInitCommand(program2) {
|
|
31895
32140
|
program2.command("init").alias("setup").description("Set up Verity in the current project (asks the setup questions, then hands off to /verity-setup)").option("--force", "Reinstall the skills even when they are already up to date (hooks are always reconciled)").option("-y, --yes", "Take the recommended answer for every question (no prompts)").option("--no-setup", "Skip the /verity-setup handoff at the end").option(
|
|
31896
32141
|
"--plugin-mode",
|
|
@@ -32001,7 +32246,7 @@ function registerInitCommand(program2) {
|
|
|
32001
32246
|
if (defaultsOnly) {
|
|
32002
32247
|
printInfo(` intensity: ${intensity} \xB7 moments: ${moments.join(", ") || "none"} (no questions asked)`);
|
|
32003
32248
|
}
|
|
32004
|
-
await scaffoldProject(step, defaultsOnly);
|
|
32249
|
+
await scaffoldProject(step, defaultsOnly, pluginMode);
|
|
32005
32250
|
const globalVerityDir = (0, import_node_path46.join)(process.env.HOME ?? "", ".verity");
|
|
32006
32251
|
await (0, import_promises20.mkdir)(globalVerityDir, { recursive: true });
|
|
32007
32252
|
console.log("");
|
|
@@ -32101,7 +32346,7 @@ function registerInitCommand(program2) {
|
|
|
32101
32346
|
...telemetryChoice ? { telemetry: telemetryChoice } : {},
|
|
32102
32347
|
init: {
|
|
32103
32348
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32104
|
-
cli_version: true ? "0.33.3-experimental.
|
|
32349
|
+
cli_version: true ? "0.33.3-experimental.86b5b6f" : "dev"
|
|
32105
32350
|
}
|
|
32106
32351
|
});
|
|
32107
32352
|
} catch (err) {
|
|
@@ -33026,6 +33271,124 @@ function registerTelemetryCommands(program2) {
|
|
|
33026
33271
|
// src/commands/prove.ts
|
|
33027
33272
|
var import_node_fs66 = require("node:fs");
|
|
33028
33273
|
var import_node_path49 = require("node:path");
|
|
33274
|
+
|
|
33275
|
+
// src/lib/prove/hint-pool.ts
|
|
33276
|
+
var STOP2 = /* @__PURE__ */ new Set([
|
|
33277
|
+
"about",
|
|
33278
|
+
"worried",
|
|
33279
|
+
"worry",
|
|
33280
|
+
"worries",
|
|
33281
|
+
"that",
|
|
33282
|
+
"this",
|
|
33283
|
+
"with",
|
|
33284
|
+
"from",
|
|
33285
|
+
"into",
|
|
33286
|
+
"code",
|
|
33287
|
+
"function",
|
|
33288
|
+
"functions",
|
|
33289
|
+
"functionality",
|
|
33290
|
+
"feature",
|
|
33291
|
+
"features",
|
|
33292
|
+
"logic",
|
|
33293
|
+
"module",
|
|
33294
|
+
"file",
|
|
33295
|
+
"files",
|
|
33296
|
+
"when",
|
|
33297
|
+
"what",
|
|
33298
|
+
"does",
|
|
33299
|
+
"could",
|
|
33300
|
+
"should",
|
|
33301
|
+
"would",
|
|
33302
|
+
"have",
|
|
33303
|
+
"been",
|
|
33304
|
+
"being",
|
|
33305
|
+
"will",
|
|
33306
|
+
"just",
|
|
33307
|
+
"like",
|
|
33308
|
+
"also",
|
|
33309
|
+
"very",
|
|
33310
|
+
"really",
|
|
33311
|
+
"make",
|
|
33312
|
+
"sure",
|
|
33313
|
+
"check",
|
|
33314
|
+
"verify",
|
|
33315
|
+
"prove",
|
|
33316
|
+
"test",
|
|
33317
|
+
"tests",
|
|
33318
|
+
"bugs",
|
|
33319
|
+
"issue",
|
|
33320
|
+
"issues",
|
|
33321
|
+
"race",
|
|
33322
|
+
"races",
|
|
33323
|
+
"there",
|
|
33324
|
+
"here",
|
|
33325
|
+
"some",
|
|
33326
|
+
"their",
|
|
33327
|
+
"your",
|
|
33328
|
+
"them",
|
|
33329
|
+
"they",
|
|
33330
|
+
"think",
|
|
33331
|
+
"want",
|
|
33332
|
+
"need",
|
|
33333
|
+
"please",
|
|
33334
|
+
"something",
|
|
33335
|
+
"anything",
|
|
33336
|
+
"part",
|
|
33337
|
+
"around"
|
|
33338
|
+
]);
|
|
33339
|
+
function hintKeywords(hint) {
|
|
33340
|
+
const out = [];
|
|
33341
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33342
|
+
for (const raw of hint.toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) ?? []) {
|
|
33343
|
+
const forms = [raw];
|
|
33344
|
+
if (raw.length > 4 && raw.endsWith("s")) forms.push(raw.slice(0, -1));
|
|
33345
|
+
for (const w of forms) {
|
|
33346
|
+
if (STOP2.has(w) || seen.has(w)) continue;
|
|
33347
|
+
seen.add(w);
|
|
33348
|
+
out.push(w);
|
|
33349
|
+
}
|
|
33350
|
+
}
|
|
33351
|
+
return out.slice(0, 8);
|
|
33352
|
+
}
|
|
33353
|
+
function hintMatches(root, hint, max = 30) {
|
|
33354
|
+
const keywords = hintKeywords(hint);
|
|
33355
|
+
if (keywords.length === 0) return { keywords, files: [] };
|
|
33356
|
+
const ls = runGitSync(root, ["ls-files", "-z", "--", "."], { timeoutMs: 3e4 });
|
|
33357
|
+
const tracked = ls.ok ? ls.stdout.split("\0").filter((f) => f && isCandidateSource(f)) : [];
|
|
33358
|
+
const files = [];
|
|
33359
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33360
|
+
const add = (f) => {
|
|
33361
|
+
if (seen.has(f) || !isCandidateSource(f)) return;
|
|
33362
|
+
seen.add(f);
|
|
33363
|
+
files.push(f);
|
|
33364
|
+
};
|
|
33365
|
+
for (const f of tracked) if (keywords.some((k) => f.toLowerCase().includes(k))) add(f);
|
|
33366
|
+
for (const k of keywords) {
|
|
33367
|
+
if (files.length >= max) break;
|
|
33368
|
+
const res = runGitSync(root, ["grep", "-i", "-l", "-e", k, "--", "."], { timeoutMs: 3e4 });
|
|
33369
|
+
if (!res.ok) continue;
|
|
33370
|
+
for (const f of res.stdout.split("\n")) if (f) add(f);
|
|
33371
|
+
}
|
|
33372
|
+
return { keywords, files: files.slice(0, max) };
|
|
33373
|
+
}
|
|
33374
|
+
|
|
33375
|
+
// src/lib/prove/pool.ts
|
|
33376
|
+
async function mapPool(items, limit, fn) {
|
|
33377
|
+
const out = new Array(items.length);
|
|
33378
|
+
let next = 0;
|
|
33379
|
+
const worker = async () => {
|
|
33380
|
+
while (next < items.length) {
|
|
33381
|
+
const i = next++;
|
|
33382
|
+
out[i] = await fn(items[i]);
|
|
33383
|
+
}
|
|
33384
|
+
};
|
|
33385
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
33386
|
+
return out;
|
|
33387
|
+
}
|
|
33388
|
+
|
|
33389
|
+
// src/commands/prove.ts
|
|
33390
|
+
var HINT_POOL_MAX = 20;
|
|
33391
|
+
var REPLAY_CONCURRENCY = 3;
|
|
33029
33392
|
async function proveAuth(program2) {
|
|
33030
33393
|
const globals = program2.opts();
|
|
33031
33394
|
const token = await resolveToken(globals.token);
|
|
@@ -33043,7 +33406,19 @@ function registerProveCommand(program2) {
|
|
|
33043
33406
|
process.exit(2);
|
|
33044
33407
|
}
|
|
33045
33408
|
const limit = Math.max(1, Math.min(50, Number(opts.limit) || 10));
|
|
33046
|
-
|
|
33409
|
+
let signals = collectRiskSignals(root, { since: opts.since, limit, paths: paths.map((p) => (0, import_node_path49.relative)(root, (0, import_node_path49.resolve)(p)) || ".") });
|
|
33410
|
+
if (opts.hint && paths.length === 0) {
|
|
33411
|
+
const hit = hintMatches(root, opts.hint);
|
|
33412
|
+
if (hit.files.length > 0) {
|
|
33413
|
+
const byFile = new Map(collectRiskSignals(root, { since: opts.since, limit: hit.files.length, paths: hit.files }).map((s) => [s.file, s]));
|
|
33414
|
+
const named = hit.files.map((f) => byFile.get(f)).filter((s) => !!s);
|
|
33415
|
+
const seen = new Set(named.map((s) => s.file));
|
|
33416
|
+
signals = [...named, ...signals.filter((s) => !seen.has(s.file))].slice(0, HINT_POOL_MAX);
|
|
33417
|
+
if (!opts.json) printInfo(`hint: ${hit.keywords.join(", ")} \u2192 ${Math.min(named.length, HINT_POOL_MAX)} file(s) named by path or content lead the pool of ${signals.length}`);
|
|
33418
|
+
} else if (!opts.json && hit.keywords.length > 0) {
|
|
33419
|
+
printInfo(`hint: no file names or contents match ${hit.keywords.join(", ")}; ranking the history pool`);
|
|
33420
|
+
}
|
|
33421
|
+
}
|
|
33047
33422
|
const outcome = await ranker.rank(signals, { root, hint: opts.hint, auth: await proveAuth(program2) ?? void 0 });
|
|
33048
33423
|
if (!outcome.ok) printWarn(`${outcome.ranker} ranker unavailable (${outcome.error}); showing the deterministic order`);
|
|
33049
33424
|
if (opts.json) {
|
|
@@ -33179,12 +33554,10 @@ ${report.error ?? ""}`);
|
|
|
33179
33554
|
return;
|
|
33180
33555
|
}
|
|
33181
33556
|
const modelSource = readRegularFileInRoot(root, (0, import_node_path49.relative)(root, file), 512 * 1024);
|
|
33182
|
-
|
|
33183
|
-
|
|
33557
|
+
if (!opts.json) printInfo(`replaying ${targets.length} counterexample(s) of ${model} in network-less containers (${Math.min(targets.length, REPLAY_CONCURRENCY)} at a time)\u2026`);
|
|
33558
|
+
const outcomes = await mapPool(targets, REPLAY_CONCURRENCY, async (p) => {
|
|
33184
33559
|
const brief = buildReplayBrief(root, report, p, modelSource.ok ? modelSource.content : "");
|
|
33185
|
-
if (!opts.json) printInfo(`replaying ${model} ${p.id} (${p.trace?.length ?? 0} steps) in a network-less container\u2026`);
|
|
33186
33560
|
const outcome = await runReplay(brief, { auth: auth2 ?? void 0, apiKey: process.env.OPENAI_API_KEY, model: opts.modelName, timeoutMs: Math.max(1e4, Number(opts.timeout) || 24e4) });
|
|
33187
|
-
outcomes.push({ property: p.id, outcome });
|
|
33188
33561
|
if (!opts.json) process.stdout.write(renderReplay(model, p, outcome) + "\n");
|
|
33189
33562
|
try {
|
|
33190
33563
|
writeReplayResult(root, report.model, p.id, outcome);
|
|
@@ -33192,7 +33565,8 @@ ${report.error ?? ""}`);
|
|
|
33192
33565
|
if (!(err instanceof SafeWriteError)) throw err;
|
|
33193
33566
|
printWarn(`${err.message} \u2014 replay result not saved`);
|
|
33194
33567
|
}
|
|
33195
|
-
|
|
33568
|
+
return { property: p.id, outcome };
|
|
33569
|
+
});
|
|
33196
33570
|
if (opts.json) printJson({ model, replays: outcomes.map((o) => ({ property: o.property, ...toStoredReplay(o.outcome) })) });
|
|
33197
33571
|
if (outcomes.some((o) => o.outcome.error)) process.exit(2);
|
|
33198
33572
|
if (outcomes.some((o) => o.outcome.verdict !== "reproduced")) process.exit(1);
|
|
@@ -33228,6 +33602,74 @@ ${f.reason ? ` ${terminalText(f.reason, 2e3)}
|
|
|
33228
33602
|
}
|
|
33229
33603
|
process.exit(outcome.findings.some((f) => f.verdict === "reproduced") ? 1 : 0);
|
|
33230
33604
|
});
|
|
33605
|
+
prove.command("generate <target>").description("Have the Verity service write a Lean model of one symbol (file#symbol), lint it, and check it here \u2014 up to 3 rounds").option("--lines <a-b>", "Model a region of the file (1-based, inclusive) instead of a named symbol").option("--hint <text>", "The user's worry, in their words (may become an intent property)").option("--revise <model>", "Revise this existing model after a diverged replay (with --note)").option("--note <text>", "What the replay showed was different (with --revise)").option("--json", "Machine-readable output").action(async (targetArg, opts) => {
|
|
33606
|
+
const root = repoRoot();
|
|
33607
|
+
const auth2 = await proveAuth(program2);
|
|
33608
|
+
if (!auth2 && !(process.env.VERITY_PROVE_DIRECT === "1" && process.env.OPENAI_API_KEY)) {
|
|
33609
|
+
printError("Not logged in \u2014 models are written by the Verity service. Run `verity login` first.");
|
|
33610
|
+
process.exit(2);
|
|
33611
|
+
}
|
|
33612
|
+
const tc = findToolchain();
|
|
33613
|
+
if (!tc) {
|
|
33614
|
+
printError(INSTALL_HINT);
|
|
33615
|
+
process.exit(2);
|
|
33616
|
+
}
|
|
33617
|
+
const hash = targetArg.indexOf("#");
|
|
33618
|
+
const file = (hash >= 0 ? targetArg.slice(0, hash) : targetArg).replace(/\\/g, "/");
|
|
33619
|
+
const symbol = hash >= 0 ? targetArg.slice(hash + 1) : "";
|
|
33620
|
+
let lines;
|
|
33621
|
+
if (opts.lines) {
|
|
33622
|
+
const m = /^(\d+)-(\d+)$/.exec(opts.lines);
|
|
33623
|
+
if (!m || Number(m[1]) < 1 || Number(m[2]) < Number(m[1])) {
|
|
33624
|
+
printError("--lines must be a-b with 1 \u2264 a \u2264 b");
|
|
33625
|
+
process.exit(2);
|
|
33626
|
+
}
|
|
33627
|
+
lines = { from: Number(m[1]), to: Number(m[2]) };
|
|
33628
|
+
}
|
|
33629
|
+
if (!file || !symbol && !lines) {
|
|
33630
|
+
printError("Target is file#symbol, or file with --lines a-b");
|
|
33631
|
+
process.exit(2);
|
|
33632
|
+
}
|
|
33633
|
+
let revise;
|
|
33634
|
+
if (opts.revise) {
|
|
33635
|
+
const existing = resolveModelFile(root, opts.revise);
|
|
33636
|
+
const src = existing ? readRegularFileInRoot(root, (0, import_node_path49.relative)(root, existing), 64 * 1024) : null;
|
|
33637
|
+
if (!src || !src.ok || !opts.note) {
|
|
33638
|
+
printError('--revise needs an existing model under Models/ and --note "<what the replay showed>"');
|
|
33639
|
+
process.exit(2);
|
|
33640
|
+
}
|
|
33641
|
+
revise = { source: src.content, note: opts.note };
|
|
33642
|
+
}
|
|
33643
|
+
refreshScaffold(root);
|
|
33644
|
+
const build = buildLibrary(tc, root);
|
|
33645
|
+
if (!build.ok) {
|
|
33646
|
+
printError("Verity library did not build");
|
|
33647
|
+
process.exit(2);
|
|
33648
|
+
}
|
|
33649
|
+
const deps = defaultDeps({ auth: auth2 ?? void 0, apiKey: process.env.VERITY_PROVE_DIRECT === "1" ? process.env.OPENAI_API_KEY : void 0, toolchain: tc, tier: "deep" });
|
|
33650
|
+
if (!opts.json) printInfo(`asking the Verity service for a model of ${symbol || `${file}:${opts.lines}`} \u2026`);
|
|
33651
|
+
const out = await generateAndCheck(root, { file, symbol: symbol || `${file}:${opts.lines}`, lines, hint: opts.hint, revise }, { deps, maxRounds: 3 });
|
|
33652
|
+
if (!out.ok) {
|
|
33653
|
+
if (opts.json) printJson(out);
|
|
33654
|
+
else printError(`${out.error} (${out.rounds} round(s)${out.costUsd ? `, $${out.costUsd.toFixed(2)}` : ""}) \u2014 write the model by hand (Models/README.md) or narrow the target`);
|
|
33655
|
+
process.exit(2);
|
|
33656
|
+
}
|
|
33657
|
+
try {
|
|
33658
|
+
writeResult(root, out.report);
|
|
33659
|
+
} catch (err) {
|
|
33660
|
+
if (!(err instanceof SafeWriteError)) throw err;
|
|
33661
|
+
}
|
|
33662
|
+
if (opts.json) {
|
|
33663
|
+
printJson({ name: out.name, file: out.file, rounds: out.rounds, cost_usd: out.costUsd, report: { ...out.report, passes: reportPasses(out.report), summary: summarize(out.report) } });
|
|
33664
|
+
return;
|
|
33665
|
+
}
|
|
33666
|
+
printInfo(`wrote ${out.file} (${out.rounds} round(s)${out.costUsd ? `, $${out.costUsd.toFixed(2)}` : ""})`);
|
|
33667
|
+
process.stdout.write(renderReport(out.report) + "\n");
|
|
33668
|
+
const s = summarize(out.report);
|
|
33669
|
+
if (s.counterexamples > 0) process.stdout.write(`${DIM}next: verity prove replay ${out.name}${NC}
|
|
33670
|
+
`);
|
|
33671
|
+
process.exit(0);
|
|
33672
|
+
});
|
|
33231
33673
|
prove.command("waive <file>").description("Dismiss the remembered formal finding(s) on a file for its current bytes \u2014 the accepted-risk path for a formal block").requiredOption("--reason <text>", "The human disposition this records (reviewer finding, ADR, \u2026)").action((file, opts) => {
|
|
33232
33674
|
const root = repoRoot();
|
|
33233
33675
|
const rel0 = (0, import_node_path49.relative)(root, (0, import_node_path49.isAbsolute)(file) ? file : (0, import_node_path49.join)(root, file)).replace(/\\/g, "/");
|
|
@@ -33338,7 +33780,15 @@ function registerLeanCommand(program2) {
|
|
|
33338
33780
|
return;
|
|
33339
33781
|
}
|
|
33340
33782
|
}
|
|
33341
|
-
const
|
|
33783
|
+
const progress = process.stderr.isTTY ? inPlaceProgress(`downloading ${asset}`) : null;
|
|
33784
|
+
const result = await installLean({
|
|
33785
|
+
onStatus: (line) => {
|
|
33786
|
+
progress?.done();
|
|
33787
|
+
printInfo(line);
|
|
33788
|
+
},
|
|
33789
|
+
...progress ? { onProgress: progress.update } : {}
|
|
33790
|
+
});
|
|
33791
|
+
progress?.done();
|
|
33342
33792
|
if (!result.ok) {
|
|
33343
33793
|
printError(result.error);
|
|
33344
33794
|
process.exit(2);
|
|
@@ -33358,8 +33808,8 @@ function registerLeanCommand(program2) {
|
|
|
33358
33808
|
}
|
|
33359
33809
|
|
|
33360
33810
|
// src/cli.ts
|
|
33361
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.
|
|
33362
|
-
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.
|
|
33811
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.86b5b6f").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) => {
|
|
33812
|
+
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.86b5b6f");
|
|
33363
33813
|
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
33364
33814
|
try {
|
|
33365
33815
|
await foldLegacyLocalCredential();
|
|
@@ -1,174 +1,135 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: >-
|
|
3
|
-
Formally verify a small, risky piece of code with a Lean 4 model
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
Formally verify a small, risky piece of code with a Lean 4 model written by the
|
|
4
|
+
Verity service: rank candidates from git history and the user's hint, CONFIRM with
|
|
5
|
+
the user which code to model, have the service generate the model, check it locally
|
|
6
|
+
with `verity prove check`, and replay any counterexample on the real code before
|
|
7
|
+
calling it a bug. Use when the user asks to verify, prove, or model-check code, or
|
|
8
|
+
asks "can this race / double run / fail open?". Anything typed after /verity-prove
|
|
9
|
+
is the user's hint: the file, function, or worry to check.
|
|
9
10
|
---
|
|
10
11
|
# /verity-prove — Formal verification of small pieces of code
|
|
11
12
|
|
|
12
13
|
**The words after `/verity-prove` are the hint.** `/verity-prove` alone means "find me
|
|
13
14
|
something worth checking"; `/verity-prove can the Stripe webhook double-charge on a
|
|
14
|
-
redelivered event?`
|
|
15
|
-
|
|
15
|
+
redelivered event?` names the target and the worry. Carry it through every step as
|
|
16
|
+
`$ARGUMENTS`.
|
|
16
17
|
|
|
17
|
-
Verity keeps **Lean 4 models of the riskiest code**, not of the codebase
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
code. When it holds you know exactly how far it was checked.
|
|
18
|
+
Verity keeps **Lean 4 models of the riskiest code**, not of the codebase: a few dozen
|
|
19
|
+
lines that abstract ONE function and check a few properties by exhaustive bounded
|
|
20
|
+
search. When a property fails you get the **shortest sequence of actions** that breaks
|
|
21
|
+
it — a hypothesis to replay on the real code.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
**The service does the work; you orchestrate and present.** Ranking, writing the model
|
|
24
|
+
and replaying its counterexample all run on the Verity service through the CLI. You
|
|
25
|
+
do not search the codebase by hand, read the Lean library, or write Lean yourself
|
|
26
|
+
unless a step below says so. Each step is one command; read its output, decide, move on.
|
|
27
|
+
Lean runs on this machine only because the user asked for it; nothing here blocks a
|
|
28
|
+
commit or a push.
|
|
26
29
|
|
|
27
30
|
## The flow
|
|
28
31
|
|
|
29
|
-
### 1.
|
|
32
|
+
### 1. Candidates — one command
|
|
30
33
|
|
|
31
34
|
```bash
|
|
32
|
-
verity prove candidates --
|
|
33
|
-
verity prove candidates --
|
|
34
|
-
verity prove candidates src/payments # scoped to a path
|
|
35
|
+
verity prove candidates --hint "$ARGUMENTS" --json # hint given
|
|
36
|
+
verity prove candidates --limit 8 --json # no hint
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
The hint **widens the pool** (files it names by path or content come first) and the
|
|
40
|
+
service ranks it, naming a symbol and the properties worth checking per file. Do not
|
|
41
|
+
search for the code yourself: if the ranker's list does not contain what the user
|
|
42
|
+
meant, say so and ask them for the file or function. A named file or function in the
|
|
43
|
+
hint skips ranking: go straight to step 2 with it.
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
own words ("can the webhook double-charge?", "does the retry ever lose the feedback?"):
|
|
45
|
+
### 2. Confirm with the user — always
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
**Do not generate a model until the user has said which code.** Present at most three
|
|
48
|
+
candidates in one short message: file, the symbol the ranker named, the first property
|
|
49
|
+
it suggested, the reason. Ask. Their answer is the scope; do not widen it. If the hint
|
|
50
|
+
is a worry in the user's words, it becomes an `intent` property — that needs no further
|
|
51
|
+
confirmation, they just gave it.
|
|
46
52
|
|
|
47
|
-
A
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
so the user sees what else the history says — but the hint decides.
|
|
53
|
+
A hint that names the code, or a candidate the user saw in an earlier session, is
|
|
54
|
+
**not** a confirmation of the exact symbol and property. Still ask, in one line:
|
|
55
|
+
"I'll model `<file>#<symbol>` for `<property>` — go ahead?" It costs the user one word.
|
|
51
56
|
|
|
52
|
-
###
|
|
57
|
+
### 3. Set up once per repo
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
pick one, name something else, or narrow the property. Their answer is the scope; do not
|
|
58
|
-
widen it.
|
|
59
|
+
```bash
|
|
60
|
+
verity prove init
|
|
61
|
+
```
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
If Lean is missing, say what will be installed and where before running
|
|
64
|
+
`verity lean install` (official release, ≈800 MB download, 2.7 GB on disk, into
|
|
65
|
+
`~/.verity/lean`). Everything else needs only `verity login`.
|
|
62
66
|
|
|
63
|
-
###
|
|
67
|
+
### 4. Generate the model — one command
|
|
64
68
|
|
|
65
69
|
```bash
|
|
66
|
-
verity prove
|
|
70
|
+
verity prove generate src/pay.ts#charge --hint "$ARGUMENTS"
|
|
71
|
+
verity prove generate src/handler.ts --lines 120-180 --hint "$ARGUMENTS" # a region
|
|
67
72
|
```
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
###
|
|
78
|
-
|
|
79
|
-
Rules that keep models small and honest:
|
|
80
|
-
|
|
81
|
-
- **One symbol.** The header names it: `Verity.model { name, abstracts := [{ file, symbol, sha256 }] }`.
|
|
82
|
-
Get the hash with `verity prove hash <file>`. `verity prove check` reports drift when the file changes.
|
|
83
|
-
When the code is a *region* rather than a named function (a branch inside a large
|
|
84
|
-
handler), add `lines := "a-b"` (1-based, inclusive) — `verity prove replay` needs it
|
|
85
|
-
to know which real lines must run.
|
|
86
|
-
- **State = only what the property reads.** Actions = the operations that can interleave
|
|
87
|
-
(a retry, a second call, a concurrent writer). Steps = what the code does, not what it should.
|
|
88
|
-
- **Properties are tagged.** `kind := .structural` for the catalogue (idempotence, no lost
|
|
89
|
-
update, status only moves forward, retries terminate, never fails open, bounded size,
|
|
90
|
-
check-then-act is atomic, no double effect). `kind := .intent` **only** when a human
|
|
91
|
-
wrote the expectation down — a doc comment, an ADR, a ticket, a Standard rule — and
|
|
92
|
-
`source :=` cites it. Never invent intent.
|
|
93
|
-
- **End with the checks.** `#eval Verity.model …` then one `#eval Verity.check …` per
|
|
94
|
-
property (bounded exploration), `Verity.checkData` for input properties, `Verity.proved`
|
|
95
|
-
only beside a real theorem. `sorry` fails the run.
|
|
96
|
-
- Keep it under ~120 lines. If it wants more, the scope is wrong — go back to step 2.
|
|
97
|
-
|
|
98
|
-
`Models/README.md` has the skeleton; `cli/tests/fixtures/proofs/SeedRunner.lean` in
|
|
99
|
-
Verity's own repo is a worked example (the VRT-243 seed-duplication bug).
|
|
100
|
-
|
|
101
|
-
### 5. Check it
|
|
74
|
+
The service writes the model, the linter fences it, this machine elaborates it, with
|
|
75
|
+
up to **3** rounds on compiler errors — all inside the command. It prints the check:
|
|
76
|
+
per property `✓ holds to bound k`, `✗ COUNTEREXAMPLE` with the trace, or `? inconclusive`.
|
|
77
|
+
Only if it ends with "no model elaborated" do you write one by hand from
|
|
78
|
+
`.verity/proofs/Models/README.md` (one symbol, ≤ ~120 lines, `#eval Verity.model …`
|
|
79
|
+
header, one `#eval Verity.check …` per property, tagged `.structural` or `.intent` with
|
|
80
|
+
`source :=`); check it with `verity prove check <Name>`.
|
|
81
|
+
|
|
82
|
+
### 5. Replay before you call it a bug — one command
|
|
102
83
|
|
|
103
84
|
```bash
|
|
104
|
-
verity prove
|
|
105
|
-
verity prove
|
|
106
|
-
verity prove check <Name> --json # for scripts
|
|
85
|
+
verity prove replay <Name> # every counterexample
|
|
86
|
+
verity prove replay <Name> --property P2 # one
|
|
107
87
|
```
|
|
108
88
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
only git-tracked files are ever read — and requires three things before accepting
|
|
138
|
-
its answer: code actually ran (a shell call with output), the real function's lines
|
|
139
|
-
appear verbatim in what it wrote, and the harness itself printed
|
|
140
|
-
`REPLAY_RESULT=reproduced`. `REPRODUCED` → the
|
|
141
|
-
real code does this. `DIVERGED` → the model is wrong about the code. `UNVERIFIED` →
|
|
142
|
-
not evidence either way. `UNREPLAYABLE` → it could not run the function faithfully.
|
|
143
|
-
Read its reason: the stubs it chose are where a false reproduction hides.
|
|
144
|
-
2. When the replay is unverified or unreplayable — or the code needs a runtime the
|
|
145
|
-
container lacks (Deno edge functions, a real database) — replay by hand: read the
|
|
146
|
-
real function again, map every trace action to a real call, and reproduce it with a
|
|
147
|
-
throwaway harness in a scratch directory that loads the real module and stubs only
|
|
148
|
-
I/O boundaries. **Never edit the user's source files to do this.**
|
|
149
|
-
3. Reproduced → report it as a bug, with the trace and the reproduction. Not reproduced
|
|
150
|
-
→ the model is wrong (`model-divergence`): say so, fix the model, do not report a bug.
|
|
151
|
-
|
|
152
|
-
This is the rule Verity applies everywhere (memory: *replay formal counterexamples
|
|
153
|
-
against changed code before blocking*). A model that finds a bug in itself is noise.
|
|
154
|
-
|
|
155
|
-
### 7. Report, in a few lines
|
|
156
|
-
|
|
157
|
-
State what was modelled, what was checked to what bound, what was refuted and whether
|
|
158
|
-
it replayed, and what remains `intent` awaiting the user's confirmation. **Never offer
|
|
159
|
-
to commit the model** — `.verity/proofs/` is machine-local and ignores itself; models
|
|
160
|
-
would only confuse reviewers. What reaches the team is the bug report and, when asked,
|
|
161
|
-
the fix. Then record the outcome:
|
|
89
|
+
The service replays the trace on the real code in a network-less container and accepts
|
|
90
|
+
an answer only when code ran, the real function's lines appear verbatim in the harness,
|
|
91
|
+
and the harness itself printed `REPLAY_RESULT=reproduced`. A model's counterexamples
|
|
92
|
+
replay in parallel, so the command takes about as long as one replay (~30 s). **Run it
|
|
93
|
+
in the foreground and wait for it**: do not background it, do other work, or report
|
|
94
|
+
before every verdict is in. Read the verdict:
|
|
95
|
+
|
|
96
|
+
- **REPRODUCED** — the real code does this. **This is final: do not replay by hand
|
|
97
|
+
as well.** Report it as a bug (step 6).
|
|
98
|
+
- **DIVERGED** — the model is wrong about the code. Revise it **at most twice**, each
|
|
99
|
+
time saying what the replay showed:
|
|
100
|
+
`verity prove generate <target> --revise <Name> --note "<what the replay showed>"`,
|
|
101
|
+
then replay again. After two divergences, report "no faithful model found for
|
|
102
|
+
`<symbol>`" — that is a valid result, not a bug.
|
|
103
|
+
- **UNVERIFIED / UNREPLAYABLE** — not evidence either way. Only now replay by hand:
|
|
104
|
+
a throwaway harness in a scratch directory that loads the real module and stubs only
|
|
105
|
+
I/O boundaries **in memory**. Never edit the user's source files, and never read from
|
|
106
|
+
or write to the user's databases, services or local stacks to do this — ask first if
|
|
107
|
+
a stub cannot be faithful.
|
|
108
|
+
|
|
109
|
+
Never weaken or delete a property to get green; never add one to make something fail.
|
|
110
|
+
|
|
111
|
+
### 6. Report, in a few lines
|
|
112
|
+
|
|
113
|
+
What was modelled, what was checked to what bound, what was refuted and whether it
|
|
114
|
+
replayed, and what remains `intent` awaiting the user's confirmation. **Never offer to
|
|
115
|
+
commit the model** — `.verity/proofs/` is machine-local and ignores itself. What reaches
|
|
116
|
+
the team is the bug report and, when asked, the fix. Then record the outcome:
|
|
162
117
|
|
|
163
118
|
```bash
|
|
164
119
|
verity reflect --user-input "Modelled runSeed (cli/src/lib/seed-runner.ts): P1 at-most-once refuted in 2 steps, replayed — guard checks a path the server never writes." --kind gotcha
|
|
165
120
|
```
|
|
166
121
|
|
|
122
|
+
The abstracted file's content leaves the machine for the Verity service and its
|
|
123
|
+
replay container; only git-tracked files are ever read.
|
|
124
|
+
|
|
167
125
|
## Do not
|
|
168
126
|
|
|
127
|
+
- Search the codebase yourself before the ranker has answered, or instead of asking the user.
|
|
169
128
|
- Model code the user did not confirm.
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
129
|
+
- Write Lean by hand while `verity prove generate` can still try.
|
|
130
|
+
- Replay by hand after a REPRODUCED verdict, or present a model-only counterexample as a bug.
|
|
131
|
+
- Background `verity prove replay`, or report before its verdicts are in.
|
|
132
|
+
- Write to the user's databases or local services, even throwaway rows.
|
|
133
|
+
- Put `sorry` in a model, weaken a property to get green, or touch source files.
|
|
134
|
+
- Run Lean from a hook, or install anything silently.
|
|
173
135
|
- Commit, stage, or un-ignore anything under `.verity/proofs/`.
|
|
174
|
-
- Model a whole module. If the state has more than ~6 fields, narrow it.
|
package/package.json
CHANGED