@codacy/verity-cli 0.33.3-experimental.86b5b6f → 0.33.3-experimental.b6d47c6
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 +492 -125
- 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.b6d47c6" : "dev";
|
|
22643
22643
|
} catch {
|
|
22644
22644
|
return "dev";
|
|
22645
22645
|
}
|
|
@@ -26825,7 +26825,7 @@ async function buildAgentDiffs(ctx, paths, limits = { perFileChars: 24e3, totalC
|
|
|
26825
26825
|
|
|
26826
26826
|
// src/lib/prove/auto.ts
|
|
26827
26827
|
var import_node_path41 = require("node:path");
|
|
26828
|
-
var
|
|
26828
|
+
var import_node_crypto16 = require("node:crypto");
|
|
26829
26829
|
|
|
26830
26830
|
// src/lib/prove/signals.ts
|
|
26831
26831
|
var DEFAULT_SINCE = "180 days ago";
|
|
@@ -26836,7 +26836,7 @@ var RISK_PATTERNS = [
|
|
|
26836
26836
|
["retry", /\b(retry|retries|retried|backoff|attempt\w*|reconnect\w*)\b/i],
|
|
26837
26837
|
["cache_or_idempotency", /\b(cache\w*|dedup\w*|idempoten\w*|memoiz\w*|debounce\w*|throttl\w*)\b/i],
|
|
26838
26838
|
["state_machine", /\b(status|transition\w*|phase|lifecycle|state\s*machine)\b/i],
|
|
26839
|
-
["money", /\b(amount|balance|price|payment\w*|charge\w*|invoice\w*|refund\w*|currency|cost_usd)\b/i],
|
|
26839
|
+
["money", /\b(amount|balance|price|payment\w*|payout\w*|charge\w*|invoice\w*|refund\w*|currency|cents|ledger\w*|wallet\w*|fee|fees|discount\w*|cost_usd)\b/i],
|
|
26840
26840
|
["auth", /\b(auth\w*|bearer|token\w*|session\w*|permission\w*|grant\w*|role\w*)\b/i],
|
|
26841
26841
|
["transaction", /\b(transaction\w*|rollback|commit\(|BEGIN|savepoint)\b/i]
|
|
26842
26842
|
];
|
|
@@ -26886,6 +26886,12 @@ function trackedSourceFiles(root, opts = {}) {
|
|
|
26886
26886
|
const files = res.stdout.split("\0").filter((f) => f && isCandidateSource(f));
|
|
26887
26887
|
return files.slice(0, opts.maxFiles ?? 5e3);
|
|
26888
26888
|
}
|
|
26889
|
+
var EFFECT_LINE = /\bawait\b|\basync\s+with\b|\.then\(|\bsetTimeout\(|\bsetInterval\(|\bwith\s+\w[\w.]*\.(?:lock|acquire)\b|\bLock\(\)/;
|
|
26890
|
+
function countEffects(text) {
|
|
26891
|
+
let n = 0;
|
|
26892
|
+
for (const line of text.split("\n")) if (EFFECT_LINE.test(line)) n++;
|
|
26893
|
+
return n;
|
|
26894
|
+
}
|
|
26889
26895
|
function scanRisk(text) {
|
|
26890
26896
|
const counts = Object.fromEntries(RISK_PATTERNS.map(([c]) => [c, 0]));
|
|
26891
26897
|
for (const line of text.split("\n")) {
|
|
@@ -26934,6 +26940,7 @@ function collectRiskSignals(root, opts = {}) {
|
|
|
26934
26940
|
let bytes = 0;
|
|
26935
26941
|
let lines = 0;
|
|
26936
26942
|
let risk = scanRisk("");
|
|
26943
|
+
let effects = 0;
|
|
26937
26944
|
const st = statRegularInRoot(root, file);
|
|
26938
26945
|
if (!st.ok) continue;
|
|
26939
26946
|
bytes = st.size;
|
|
@@ -26942,6 +26949,7 @@ function collectRiskSignals(root, opts = {}) {
|
|
|
26942
26949
|
if (read.ok) {
|
|
26943
26950
|
lines = read.content.split("\n").length;
|
|
26944
26951
|
risk = scanRisk(read.content);
|
|
26952
|
+
effects = countEffects(read.content);
|
|
26945
26953
|
}
|
|
26946
26954
|
}
|
|
26947
26955
|
const base = {
|
|
@@ -26952,7 +26960,8 @@ function collectRiskSignals(root, opts = {}) {
|
|
|
26952
26960
|
lastTouched: h?.lastTouched ?? null,
|
|
26953
26961
|
bytes,
|
|
26954
26962
|
lines,
|
|
26955
|
-
risk
|
|
26963
|
+
risk,
|
|
26964
|
+
effects
|
|
26956
26965
|
};
|
|
26957
26966
|
const { score, reasons } = scoreSignals(base);
|
|
26958
26967
|
if (score <= 0) continue;
|
|
@@ -27036,10 +27045,14 @@ var LUNA_SCHEMA = {
|
|
|
27036
27045
|
},
|
|
27037
27046
|
required: ["items"]
|
|
27038
27047
|
};
|
|
27039
|
-
var LUNA_INSTRUCTIONS = `You
|
|
27040
|
-
For each file you
|
|
27041
|
-
|
|
27042
|
-
|
|
27048
|
+
var LUNA_INSTRUCTIONS = `You decide whether a push deserves a FORMAL MODEL: a Lean 4 transition system explored over every interleaving, retry, failure and crash, then replayed on the real code. It costs the developer about a minute of their push, and blocks it only on a bug reproduced on the real code. It is a scalpel: most pushes deserve NO model.
|
|
27049
|
+
For each file you get its git-history signals (commits, fix commits), risk keyword counts and an excerpt. Score each 0-1 for "a model of ONE function here is worth that minute, because it would likely expose a real, costly bug". A high score needs ALL THREE:
|
|
27050
|
+
- CRITICAL: a mistake costs money, data, access or a stuck workflow (payments, balances, stock, ledgers, auth and permissions, jobs and queues, sync, status machines, anything that must happen exactly once);
|
|
27051
|
+
- A HAZARD a model can explore: two or more effects that can interleave (read then write, check then act), a retry or a redelivery, an external call whose failure path matters, a lock, a lease, a cache, a cursor, a crash window between writes \u2014 or pure arithmetic on money or counts;
|
|
27052
|
+
- ERROR-PRONE or COMPLEX: fix commits in its history, high churn, several branches or error paths, a concurrency primitive \u2014 a hazard nobody sees by reading the function once.
|
|
27053
|
+
Score LOW (below 0.5) when any is missing: rendering, formatting, configuration, command registration, wiring, types, a single read, a single write with nothing before it, logging, or a function whose correctness is plain from reading it. A crash window alone (an external call, then a local write, where a re-run repairs the state and nothing is lost for good) is not enough for a high score. A model of a function longer than about 150 lines abstracts too much to find anything: name the smaller function inside it that holds the hazard, or score lower.
|
|
27054
|
+
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 that names the hazard. Return every file you were given, exactly once.
|
|
27055
|
+
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, never the edit itself. A one-line change (a log line, a rename) to a function that meets all three still scores high: a latent bug in it is exactly what this is for. A large change to a function that misses one still scores low.`;
|
|
27043
27056
|
var lunaRanker = {
|
|
27044
27057
|
name: "luna",
|
|
27045
27058
|
async rank(candidates2, ctx) {
|
|
@@ -27462,6 +27475,7 @@ function braceDelta(code) {
|
|
|
27462
27475
|
}
|
|
27463
27476
|
|
|
27464
27477
|
// src/lib/prove/generate.ts
|
|
27478
|
+
var MAX_NUMBERED_BYTES = 64 * 1024;
|
|
27465
27479
|
var LIBRARY_API = `The Verity Lean 4 library (already built; \`import Verity\` is the ONLY import allowed):
|
|
27466
27480
|
|
|
27467
27481
|
structure Verity.System (\u03C3 \u03B1 : Type) where
|
|
@@ -27480,7 +27494,8 @@ var LIBRARY_API = `The Verity Lean 4 library (already built; \`import Verity\` i
|
|
|
27480
27494
|
Verity.checkData [ToString \u03B2] (m : Model) (p : Property) (inputs : List \u03B2) (prop : \u03B2 \u2192 Bool) : IO Unit
|
|
27481
27495
|
Verity.proved (m : Model) (p : Property) (theoremName : String) : IO Unit -- only beside a real theorem
|
|
27482
27496
|
|
|
27483
|
-
\`Verity.check\` explores every interleaving of enabled actions breadth-first to \`bound\` and reports the SHORTEST action sequence reaching a state where \`inv\` is false
|
|
27497
|
+
\`Verity.check\` explores every interleaving of enabled actions breadth-first to \`bound\` and reports the SHORTEST action sequence reaching a state where \`inv\` is false.
|
|
27498
|
+
\`Verity.checkData\` evaluates \`prop\` on each concrete input and reports the first that fails \u2014 for a PURE computation (exemplar 3).`;
|
|
27484
27499
|
var EXEMPLAR = `import Verity
|
|
27485
27500
|
|
|
27486
27501
|
namespace Models.SeedRunner
|
|
@@ -27524,51 +27539,272 @@ def P1 : Property := { id := "P1", kind := .structural, desc := "seeding runs at
|
|
|
27524
27539
|
#eval Verity.check model P1 sys (fun s => s.posts \u2264 1) { bound := 4 }
|
|
27525
27540
|
|
|
27526
27541
|
end Models.SeedRunner`;
|
|
27527
|
-
var
|
|
27542
|
+
var EXEMPLAR_CONCURRENT = `import Verity
|
|
27543
|
+
|
|
27544
|
+
namespace Models.Withdraw
|
|
27545
|
+
open Verity
|
|
27546
|
+
|
|
27547
|
+
-- withdraw(account, 7) called twice at once on a balance of 10. Each call has
|
|
27548
|
+
-- its own program counter: 0 not started, 1 read the balance, 2 debited, 3 done.
|
|
27549
|
+
structure S where
|
|
27550
|
+
balance : Nat
|
|
27551
|
+
pcA : Nat
|
|
27552
|
+
seenA : Nat
|
|
27553
|
+
pcB : Nat
|
|
27554
|
+
seenB : Nat
|
|
27555
|
+
paidOut : Nat
|
|
27556
|
+
deriving BEq, Hashable, Repr
|
|
27557
|
+
|
|
27558
|
+
instance : ToString S where
|
|
27559
|
+
toString s := s!"balance={s.balance} pcA={s.pcA} pcB={s.pcB} paidOut={s.paidOut}"
|
|
27560
|
+
|
|
27561
|
+
inductive A where
|
|
27562
|
+
| readA | debitA | recordA | readB | debitB | recordB
|
|
27563
|
+
deriving Repr
|
|
27564
|
+
|
|
27565
|
+
instance : ToString A where
|
|
27566
|
+
toString
|
|
27567
|
+
| .readA => "call A: db.getAccount (L7)"
|
|
27568
|
+
| .debitA => "call A: balance check + db.setBalance (L8-9)"
|
|
27569
|
+
| .recordA => "call A: db.recordTransaction (L10)"
|
|
27570
|
+
| .readB => "call B: db.getAccount (L7)"
|
|
27571
|
+
| .debitB => "call B: balance check + db.setBalance (L8-9)"
|
|
27572
|
+
| .recordB => "call B: db.recordTransaction (L10)"
|
|
27573
|
+
|
|
27574
|
+
def amount : Nat := 7
|
|
27575
|
+
|
|
27576
|
+
def step (s : S) : A \u2192 S
|
|
27577
|
+
| .readA => { s with pcA := 1, seenA := s.balance }
|
|
27578
|
+
| .debitA => if s.seenA < amount then { s with pcA := 3 } else { s with balance := s.seenA - amount, pcA := 2 }
|
|
27579
|
+
| .recordA => { s with paidOut := s.paidOut + amount, pcA := 3 }
|
|
27580
|
+
| .readB => { s with pcB := 1, seenB := s.balance }
|
|
27581
|
+
| .debitB => if s.seenB < amount then { s with pcB := 3 } else { s with balance := s.seenB - amount, pcB := 2 }
|
|
27582
|
+
| .recordB => { s with paidOut := s.paidOut + amount, pcB := 3 }
|
|
27583
|
+
|
|
27584
|
+
def next (pc : Nat) (read debit record : A) : List A :=
|
|
27585
|
+
if pc == 0 then [read] else if pc == 1 then [debit] else if pc == 2 then [record] else []
|
|
27586
|
+
|
|
27587
|
+
def sys : System S A :=
|
|
27588
|
+
{ init := { balance := 10, pcA := 0, seenA := 0, pcB := 0, seenB := 0, paidOut := 0 }
|
|
27589
|
+
actions := fun s => next s.pcA A.readA A.debitA A.recordA ++ next s.pcB A.readB A.debitB A.recordB
|
|
27590
|
+
step := step }
|
|
27591
|
+
|
|
27592
|
+
def model : Model :=
|
|
27593
|
+
{ name := "Withdraw"
|
|
27594
|
+
abstracts := [ { file := "src/accounts/withdraw.ts", symbol := "withdraw", sha256 := "<sha256>", lines := "5-12" } ] }
|
|
27595
|
+
|
|
27596
|
+
def P1 : Property := { id := "no_overdraft_under_concurrent_withdrawals", kind := .structural, desc := "two concurrent withdrawals never pay out more than the balance held: the balance check and the debit act as one step" }
|
|
27597
|
+
def P2 : Property := { id := "balance_matches_payouts", kind := .structural, desc := "once both calls are done, the balance plus everything paid out equals the starting balance (no lost update)" }
|
|
27598
|
+
|
|
27599
|
+
#eval Verity.model model
|
|
27600
|
+
#eval Verity.check model P1 sys (fun s => s.paidOut \u2264 10) { bound := 6 }
|
|
27601
|
+
#eval Verity.check model P2 sys (fun s => !(s.pcA == 3 && s.pcB == 3) || s.balance + s.paidOut == 10) { bound := 6 }
|
|
27602
|
+
|
|
27603
|
+
end Models.Withdraw`;
|
|
27604
|
+
var EXEMPLAR_DATA = `import Verity
|
|
27605
|
+
|
|
27606
|
+
namespace Models.Split
|
|
27607
|
+
open Verity
|
|
27608
|
+
|
|
27609
|
+
-- splitPayout(total, shares): each part rounded on its own (lines 4-6).
|
|
27610
|
+
def roundDiv (n d : Nat) : Nat := (2 * n + d) / (2 * d)
|
|
27611
|
+
|
|
27612
|
+
def split (total : Nat) (shares : List Nat) : List Nat :=
|
|
27613
|
+
let sum := shares.foldl (fun a b => a + b) 0
|
|
27614
|
+
shares.map (fun s => roundDiv (total * s) sum)
|
|
27615
|
+
|
|
27616
|
+
def model : Model :=
|
|
27617
|
+
{ name := "Split"
|
|
27618
|
+
abstracts := [ { file := "src/payouts/split.ts", symbol := "splitPayout", sha256 := "<sha256>", lines := "4-8" } ] }
|
|
27619
|
+
|
|
27620
|
+
def P1 : Property := { id := "parts_sum_to_total", kind := .structural, desc := "the parts of a payout always add up to the total paid out" }
|
|
27621
|
+
|
|
27622
|
+
#eval Verity.model model
|
|
27623
|
+
#eval Verity.checkData model P1 [(100, [1, 1, 1]), (2, [1, 1, 1]), (10, [1, 2]), (7, [3, 4])] (fun (x : Nat \xD7 List Nat) => (split x.1 x.2).foldl (fun a b => a + b) 0 == x.1)
|
|
27624
|
+
|
|
27625
|
+
end Models.Split`;
|
|
27626
|
+
var RULES = `Write ONE Lean 4 model of the code below that could expose a real bug in it. Work in three steps.
|
|
27627
|
+
|
|
27628
|
+
STEP 1 \u2014 Effects. The input lists the target's EFFECT SITES: awaits, calls on its dependencies, loops, try/catch, locks. Those are the only points where the real world can interleave, fail or crash. Each one that reads or writes shared state becomes an action, named after the real call and its line \u2014 "db.setBalance (L14)" \u2014 so a trace reads as a scenario someone can replay.
|
|
27629
|
+
|
|
27630
|
+
STEP 2 \u2014 The environment's moves that exist in reality for THIS code, and only those:
|
|
27631
|
+
\u2022 a SECOND INVOCATION running concurrently (request handlers, jobs, webhooks, workers \u2014 anything a server calls). Give each invocation its own program counter so their effects interleave (exemplar 2);
|
|
27632
|
+
\u2022 a REDELIVERY or RETRY of the same input \u2014 when the input carries an id or key, or a caller, a job or a loop retries;
|
|
27633
|
+
\u2022 an external call that TAKES EFFECT AND THEN FAILS or times out \u2014 for every external call inside a try or a retry loop: the caller sees an error although the effect happened;
|
|
27634
|
+
\u2022 TIME passing \u2014 when the code reads a clock, a lease, a TTL or an expiry;
|
|
27635
|
+
\u2022 a LOCK held across an await \u2014 acquire and release as separate actions, so two invocations can wait on each other;
|
|
27636
|
+
\u2022 a CRASH between two writes that are not one transaction, followed by the RECOVERY the code relies on (the redelivery, the job's next run, the client's retry).
|
|
27637
|
+
Keep it small: at most 2 invocations, 1 redelivery, 1 failure, 1 crash, a clock of 3 ticks.
|
|
27638
|
+
|
|
27639
|
+
STEP 3 \u2014 Properties: 2 to 4, in this order of priority:
|
|
27640
|
+
1. what the code's own comments, names or messages promise ("at most once", "dedup", "only", "must", "never", "every");
|
|
27641
|
+
2. no double effect, no lost update, check-then-act is atomic \u2014 under the second invocation or the redelivery;
|
|
27642
|
+
3. never fails open \u2014 an error, a missing row or a failed lookup never grants access, marks work done or reports success;
|
|
27643
|
+
4. conservation \u2014 money, stock and counts add up: parts sum to the total, nothing credited twice or lost, no row skipped;
|
|
27644
|
+
5. progress \u2014 no deadlock, nothing stuck forever, retries end;
|
|
27645
|
+
6. crash consistency \u2014 AT MOST ONE, listed LAST: "after a crash between X and Y and the recovery the code relies on, \u2026", or, where nothing recovers, only when the crash loses money or records for good.
|
|
27646
|
+
A property must be something the code PROMISES. An operation whose job is to apply an effect on every call (a withdrawal, a charge, a send) promises "no double effect" only where the code dedups it (an idempotency key, a unique insert, a conditional update). When a comment says duplicates are by design (consumers dedupe on an id), do not claim "no double effect". An upsert, a set-to-value or a delete applied twice is not a double effect.
|
|
27647
|
+
A counterexample must run the real function: a trace made only of environment moves (another writer, a clock) says nothing about this code. A promise about the END result (nothing lost, nothing charged twice, totals add up) is checked on states where every invocation has finished or crashed \u2014 guard the \`inv\` with the program counters, as exemplar 2's second property does; a violation that exists only while work is still pending is not a bug.
|
|
27648
|
+
A property is about an OUTCOME someone would call a bug \u2014 money, stock or records wrong, access granted, work lost or done twice, a job stuck forever \u2014 never about the mechanism's internals (a key's format, two timestamps being distinct, a lease's exact boundary tick). A comment that says HOW the code works ("make each attempt's key unique") is not the promise; the promise is what that mechanism is for.
|
|
27649
|
+
A comment that states a SAFE design choice ("a spent token presented again ends every session", "consumers dedupe, so publishing twice is harmless") is the design: never claim the opposite. A comment never makes an UNSAFE outcome safe: granting access on an error, losing or double-counting money, stock or records is a bug whatever the comment says ("don't block people when the permission service is flaky" describes a fail-open bug).
|
|
27650
|
+
Property ids name the promise in snake_case (refund_total_within_capture) \u2014 never P1. \`desc\` states the promise in the code's own terms and names the move the trace needs ("two concurrent calls \u2026", "after the charge succeeded and the call timed out \u2026").
|
|
27651
|
+
A PURE computation (no effect sites) is checked with \`Verity.checkData\` on small concrete inputs, including the edge cases (exemplar 3).
|
|
27652
|
+
|
|
27653
|
+
Lean rules \u2014 the linter and the compiler refuse anything else:
|
|
27528
27654
|
- The file starts with \`import Verity\` (the only import), then \`namespace Models.<Name>\`, \`open Verity\`, and ends with \`end Models.<Name>\`.
|
|
27529
|
-
- State: a structure with at most
|
|
27530
|
-
-
|
|
27531
|
-
- \`step\` must do what the code DOES (read the source), not what it should do.
|
|
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\`.
|
|
27655
|
+
- State: a structure with at most 8 fields, only what the properties read; \`deriving BEq, Hashable, Repr\` and a \`ToString\` instance. Keep every counter bounded through \`actions\` so the reachable state space stays finite (a few hundred states).
|
|
27656
|
+
- \`step\` does what the code DOES, line by line \u2014 not what it should do. An \`inv\` must be a real claim the explorer can refute; no tautologies. Add an \`intent\` property only when a comment states the expectation, and cite its line in \`source\`.
|
|
27533
27657
|
- The header: \`abstracts := [{ file, symbol, sha256, lines }]\` with the exact values given below (lines "" when none).
|
|
27534
|
-
- Exactly one \`#eval Verity.model\` and one \`#eval Verity.check\` per property, \`{ bound :=
|
|
27658
|
+
- Exactly one \`#eval Verity.model\` and one \`#eval Verity.check\` (or \`Verity.checkData\`) per property, \`{ bound := 8 }\` or less. No other \`#eval\`, no \`IO\`, no \`sorry\`, no attributes, no macros, no \`partial\`, no \`open\` other than \`open Verity\`, no name from \`System\`, \`Lean\` or \`Std\`. Under 150 lines.
|
|
27535
27659
|
- 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
|
|
27537
|
-
- Imports beyond \`Verity\` are refused by a linter; a file that does not elaborate gets ONE more attempt with the compiler's message.
|
|
27660
|
+
- A file that does not elaborate gets ONE more attempt with the compiler's message.
|
|
27538
27661
|
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>"}.`;
|
|
27539
|
-
var
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27545
|
-
|
|
27546
|
-
|
|
27547
|
-
|
|
27548
|
-
|
|
27662
|
+
var EFFECT_LINE2 = /\bawait\b|\basync\s+with\b|\bwith\s+\w[\w.]*\.(?:lock|acquire)\b|\.then\(|\bsetTimeout\(|\bsetInterval\(|\bqueueMicrotask\(/;
|
|
27663
|
+
var CONTROL_LINE = /^\s*(?:\}\s*)?(?:for|while|try|catch|except|finally|do)\b|^\s*(?:async\s+)?for\b/;
|
|
27664
|
+
var NUMBERED_LINE = /^\s*(\d+)\| ?(.*)$/;
|
|
27665
|
+
var PARAM_NAME = /(?:^|[(,])\s*(?:self\s*,\s*)?([A-Za-z_$][\w$]*)\s*(?=[?:=,)])/g;
|
|
27666
|
+
var MAX_EFFECT_SITES = 40;
|
|
27667
|
+
var IDENT_CHAR = /[\w$.]/;
|
|
27668
|
+
var CALL_AFTER_DOT = /^([A-Za-z_$][\w$]*)\s*\(/;
|
|
27669
|
+
var PURE_METHODS = /* @__PURE__ */ new Set([
|
|
27670
|
+
"map",
|
|
27671
|
+
"filter",
|
|
27672
|
+
"reduce",
|
|
27673
|
+
"forEach",
|
|
27674
|
+
"flatMap",
|
|
27675
|
+
"slice",
|
|
27676
|
+
"join",
|
|
27677
|
+
"split",
|
|
27678
|
+
"includes",
|
|
27679
|
+
"indexOf",
|
|
27680
|
+
"some",
|
|
27681
|
+
"every",
|
|
27682
|
+
"find",
|
|
27683
|
+
"findIndex",
|
|
27684
|
+
"concat",
|
|
27685
|
+
"sort",
|
|
27686
|
+
"toFixed",
|
|
27687
|
+
"toString",
|
|
27688
|
+
"trim",
|
|
27689
|
+
"toLowerCase",
|
|
27690
|
+
"toUpperCase",
|
|
27691
|
+
"replace",
|
|
27692
|
+
"startsWith",
|
|
27693
|
+
"endsWith",
|
|
27694
|
+
"at",
|
|
27695
|
+
"keys",
|
|
27696
|
+
"values",
|
|
27697
|
+
"entries",
|
|
27698
|
+
"items",
|
|
27699
|
+
"copy",
|
|
27700
|
+
"format",
|
|
27701
|
+
"strip",
|
|
27702
|
+
"lower",
|
|
27703
|
+
"upper",
|
|
27704
|
+
"append",
|
|
27705
|
+
"extend",
|
|
27706
|
+
"push",
|
|
27707
|
+
"pop",
|
|
27708
|
+
"has",
|
|
27709
|
+
"size",
|
|
27710
|
+
"length",
|
|
27711
|
+
"isArray",
|
|
27712
|
+
"padStart",
|
|
27713
|
+
"padEnd",
|
|
27714
|
+
"repeat",
|
|
27715
|
+
"test",
|
|
27716
|
+
"match"
|
|
27717
|
+
]);
|
|
27718
|
+
function callsOn(text, name) {
|
|
27719
|
+
const needle = `${name}.`;
|
|
27720
|
+
for (let i = text.indexOf(needle); i >= 0; i = text.indexOf(needle, i + 1)) {
|
|
27721
|
+
if (i > 0 && IDENT_CHAR.test(text[i - 1])) continue;
|
|
27722
|
+
const call = CALL_AFTER_DOT.exec(text.slice(i + needle.length));
|
|
27723
|
+
if (call && !PURE_METHODS.has(call[1])) return true;
|
|
27724
|
+
}
|
|
27725
|
+
return false;
|
|
27726
|
+
}
|
|
27727
|
+
function callsDirect(text, name) {
|
|
27728
|
+
const needle = `${name}(`;
|
|
27729
|
+
for (let i = text.indexOf(needle); i >= 0; i = text.indexOf(needle, i + 1)) {
|
|
27730
|
+
if (i > 0 && IDENT_CHAR.test(text[i - 1])) continue;
|
|
27731
|
+
return true;
|
|
27732
|
+
}
|
|
27733
|
+
return false;
|
|
27734
|
+
}
|
|
27735
|
+
function effectSites(numbered) {
|
|
27736
|
+
const rows = [];
|
|
27737
|
+
for (const raw of numbered.split("\n")) {
|
|
27738
|
+
const m = NUMBERED_LINE.exec(raw);
|
|
27739
|
+
if (m) rows.push({ n: Number(m[1]), text: m[2] });
|
|
27740
|
+
}
|
|
27741
|
+
if (rows.length === 0) return [];
|
|
27742
|
+
const head = rows.slice(0, 6).map((r) => r.text).join(" ");
|
|
27743
|
+
const open = head.indexOf("(");
|
|
27744
|
+
const close = open >= 0 ? head.indexOf(")", open) : -1;
|
|
27745
|
+
const params = /* @__PURE__ */ new Set();
|
|
27746
|
+
if (open >= 0 && close > open) {
|
|
27747
|
+
for (const m of `(${head.slice(open + 1, close)})`.matchAll(PARAM_NAME)) if (m[1] !== "self") params.add(m[1]);
|
|
27748
|
+
}
|
|
27749
|
+
const out = [];
|
|
27750
|
+
for (const [i, r] of rows.entries()) {
|
|
27751
|
+
if (i === 0) continue;
|
|
27752
|
+
const t = r.text.trim();
|
|
27753
|
+
if (!t || t.startsWith("//") || t.startsWith("#") || t.startsWith("*")) continue;
|
|
27754
|
+
const onParam = [...params].some((p) => callsOn(t, p) || callsDirect(t, p));
|
|
27755
|
+
const kind = EFFECT_LINE2.test(t) || onParam ? "effect" : CONTROL_LINE.test(t) ? "control" : null;
|
|
27756
|
+
if (!kind) continue;
|
|
27757
|
+
out.push(`L${r.n} [${kind}] ${t.length > 160 ? t.slice(0, 157) + "\u2026" : t}`);
|
|
27758
|
+
if (out.length >= MAX_EFFECT_SITES) break;
|
|
27759
|
+
}
|
|
27760
|
+
return out;
|
|
27761
|
+
}
|
|
27549
27762
|
function renderGenerateInput(t, previous) {
|
|
27550
27763
|
const parts = [];
|
|
27551
27764
|
parts.push(`# Target: ${t.file} \u2014 \`${t.symbol}\`${t.lines ? ` (lines ${t.lines.from}\u2013${t.lines.to})` : ""}`);
|
|
27552
27765
|
parts.push(`Header values: file := "${t.file}", symbol := "${t.symbol}", sha256 := "${t.sha256}", lines := "${t.lines ? `${t.lines.from}-${t.lines.to}` : ""}"`);
|
|
27553
27766
|
if (t.reasons.length) parts.push(`Why it was selected: ${t.reasons.join(" \xB7 ")}`);
|
|
27554
27767
|
if (t.properties.length) parts.push(`Suggested properties: ${t.properties.join(", ")}`);
|
|
27555
|
-
|
|
27556
|
-
|
|
27557
|
-
parts.push(
|
|
27558
|
-
|
|
27768
|
+
const numbered = t.numbered.slice(0, MAX_NUMBERED_BYTES);
|
|
27769
|
+
const sites = effectSites(numbered);
|
|
27770
|
+
parts.push("", sites.length ? `Effect sites (STEP 1 \u2014 where the real world can interleave, fail or crash):
|
|
27771
|
+
${sites.map((s) => ` ${s}`).join("\n")}` : "Effect sites: none \u2014 a pure computation (STEP 3: Verity.checkData on concrete inputs).");
|
|
27772
|
+
parts.push("", "```", numbered, "```");
|
|
27559
27773
|
if (previous) {
|
|
27560
|
-
parts.push("");
|
|
27561
|
-
parts.push("# Your previous attempt was not accepted \u2014 the diagnostics say why. Fix it (same model, same properties):");
|
|
27562
|
-
parts.push("```lean");
|
|
27563
|
-
parts.push(previous.source);
|
|
27564
|
-
parts.push("```");
|
|
27565
|
-
parts.push("Diagnostics:");
|
|
27566
|
-
parts.push("```");
|
|
27567
|
-
parts.push(previous.error.slice(0, 4e3));
|
|
27568
|
-
parts.push("```");
|
|
27774
|
+
parts.push("", "# Your previous attempt was not accepted \u2014 the diagnostics say why. Fix it (same model, same properties):", "```lean", previous.source.slice(0, 32 * 1024), "```", "Diagnostics:", "```", previous.error.slice(0, 4e3), "```");
|
|
27569
27775
|
}
|
|
27570
27776
|
return parts.join("\n");
|
|
27571
27777
|
}
|
|
27778
|
+
function generateSystemPrompt() {
|
|
27779
|
+
return `${RULES}
|
|
27780
|
+
|
|
27781
|
+
${LIBRARY_API}
|
|
27782
|
+
|
|
27783
|
+
# Exemplar 1 \u2014 one call repeated (a real model that found a real bug)
|
|
27784
|
+
\`\`\`lean
|
|
27785
|
+
${EXEMPLAR}
|
|
27786
|
+
\`\`\`
|
|
27787
|
+
|
|
27788
|
+
# Exemplar 2 \u2014 two concurrent invocations, each with its own program counter
|
|
27789
|
+
\`\`\`lean
|
|
27790
|
+
${EXEMPLAR_CONCURRENT}
|
|
27791
|
+
\`\`\`
|
|
27792
|
+
|
|
27793
|
+
# Exemplar 3 \u2014 a pure computation checked on concrete inputs
|
|
27794
|
+
\`\`\`lean
|
|
27795
|
+
${EXEMPLAR_DATA}
|
|
27796
|
+
\`\`\``;
|
|
27797
|
+
}
|
|
27798
|
+
var SCHEMA = {
|
|
27799
|
+
type: "object",
|
|
27800
|
+
additionalProperties: false,
|
|
27801
|
+
properties: {
|
|
27802
|
+
name: { type: "string" },
|
|
27803
|
+
lean_source: { type: "string" },
|
|
27804
|
+
summary: { type: "string" }
|
|
27805
|
+
},
|
|
27806
|
+
required: ["name", "lean_source", "summary"]
|
|
27807
|
+
};
|
|
27572
27808
|
function acceptAnswer(parsed, usage, costUsd = null) {
|
|
27573
27809
|
const name = typeof parsed.name === "string" && /^[A-Z][A-Za-z0-9]{1,40}$/.test(parsed.name) ? parsed.name : null;
|
|
27574
27810
|
const source = typeof parsed.lean_source === "string" ? joinBrokenLiterals(parsed.lean_source.replace(/\r\n/g, "\n")) : null;
|
|
@@ -27603,14 +27839,7 @@ async function generateModel(target, opts) {
|
|
|
27603
27839
|
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
27604
27840
|
const body = {
|
|
27605
27841
|
model: opts.model ?? (opts.tier === "fast" ? "gpt-6-luna" : "gpt-5.6-terra"),
|
|
27606
|
-
instructions:
|
|
27607
|
-
|
|
27608
|
-
${LIBRARY_API}
|
|
27609
|
-
|
|
27610
|
-
# Exemplar (a real model that found a real bug)
|
|
27611
|
-
\`\`\`lean
|
|
27612
|
-
${EXEMPLAR}
|
|
27613
|
-
\`\`\``,
|
|
27842
|
+
instructions: generateSystemPrompt(),
|
|
27614
27843
|
input: [{ role: "user", content: renderGenerateInput(target, opts.previous) }],
|
|
27615
27844
|
reasoning: { effort: opts.tier === "fast" ? "low" : "medium" },
|
|
27616
27845
|
max_output_tokens: 12e3,
|
|
@@ -27660,6 +27889,23 @@ function declaresSymbol(line, symbol, py) {
|
|
|
27660
27889
|
if (py) return PY_DECL.exec(line)?.[1] === symbol;
|
|
27661
27890
|
return TS_DECLS.some((re) => re.exec(line)?.[1] === symbol);
|
|
27662
27891
|
}
|
|
27892
|
+
function harnessInvokes(texts, symbol, py) {
|
|
27893
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(symbol)) return null;
|
|
27894
|
+
let declared = false;
|
|
27895
|
+
for (const t of texts) {
|
|
27896
|
+
for (const line of t.split("\n")) {
|
|
27897
|
+
if (declaresSymbol(line, symbol, py)) {
|
|
27898
|
+
declared = true;
|
|
27899
|
+
continue;
|
|
27900
|
+
}
|
|
27901
|
+
for (let i = line.indexOf(`${symbol}(`); i >= 0; i = line.indexOf(`${symbol}(`, i + 1)) {
|
|
27902
|
+
if (i === 0 || !/[\w$]/.test(line[i - 1])) return true;
|
|
27903
|
+
}
|
|
27904
|
+
}
|
|
27905
|
+
}
|
|
27906
|
+
return declared ? false : null;
|
|
27907
|
+
}
|
|
27908
|
+
var NOT_INVOKED = "the harness never calls the function under test \u2014 the trace was replayed on the stubs, not on the code";
|
|
27663
27909
|
function extractSymbolBody(source, symbol, file, range) {
|
|
27664
27910
|
const lines = source.split("\n");
|
|
27665
27911
|
if (range) {
|
|
@@ -27735,13 +27981,22 @@ function buildReplayBrief(root, report, property, modelSource, readSource) {
|
|
|
27735
27981
|
var REPLAY_INSTRUCTIONS = `You replay a formal-model counterexample on the REAL code, inside a disposable container with no network. Your job is to find out whether the real code does what the model says \u2014 not to fix it, not to judge whether it matters.
|
|
27736
27982
|
|
|
27737
27983
|
Rules:
|
|
27738
|
-
1. Copy the real function(s) into the container VERBATIM from the numbered source you are given
|
|
27739
|
-
2. Stub ONLY I/O boundaries: database/network clients, filesystem, clock, randomness, environment. Stubs must be faithful to the semantics the trace depends on (
|
|
27740
|
-
3. Drive the
|
|
27741
|
-
|
|
27742
|
-
|
|
27743
|
-
|
|
27744
|
-
|
|
27984
|
+
1. Copy the real function(s) into the container VERBATIM from the numbered source you are given. Do not retype, simplify or "clean up" the function under test. Copying the whole file is fine. When the source is given as a LINE RANGE (a region inside a larger handler), copy EVERY line of that range verbatim as the body of a function you declare around it, and satisfy the helpers, imports and variables it uses by defining stubs OUTSIDE the copied region. Extracting only the statements you believe matter is not acceptable: the acceptance check counts the region's lines in your harness, and a partial copy fails it.
|
|
27985
|
+
2. Stub ONLY I/O boundaries: database/network clients, filesystem, clock, randomness, environment, imported helpers. Stubs must be faithful to the semantics the trace depends on and to what the source's comments say the real call does (a unique index, a conditional UPDATE, a WHERE clause, an ordering) \u2014 and to what the code PASSES them: a payment or message stub given an idempotency key returns the first result for a repeated key and applies nothing twice; a unique key refuses a duplicate. A stub that ignores what the real service guarantees makes the reproduction worthless. If you must stub something the trace depends on in a way the source does not describe, say so in the reason.
|
|
27986
|
+
3. Drive the trace's actions in order. The model's action names cite the real calls and their lines. Environment moves, and how to reproduce them:
|
|
27987
|
+
- Two concurrent invocations: start both calls of the real function; make the stub methods they await pause on gates the driver opens, so their effects happen in the trace's order. Node: const gate = () => { let open; const p = new Promise((r) => (open = r)); return { p, open } }. Python asyncio: asyncio.Event. Python threads: threading.Event.
|
|
27988
|
+
- A redelivery or a retry: call the real function again with the same input, on the same stub state.
|
|
27989
|
+
- A call that takes effect and then fails: the stub applies the effect, then throws the error the code handles (a timeout).
|
|
27990
|
+
- Time passing: the stub clock or the injected now() returns the trace's times.
|
|
27991
|
+
- A crash between X and Y: the stub for Y throws a crash error once; treat that invocation as dead, then run the recovery the trace names (the redelivery, the job's next run).
|
|
27992
|
+
- A pure computation: call the real function on the counterexample's input.
|
|
27993
|
+
- A deadlock or a hang: run under a timeout (asyncio.wait_for, Promise.race with a timer) and treat the timeout as the observation.
|
|
27994
|
+
The harness must CALL the real function: a trace replayed on the stubs alone is not a reproduction \u2014 answer unreplayable.
|
|
27995
|
+
4. COMPUTE the verdict in the harness: print the observable state after each action; let every timer, promise and retry the trace did not crash or cancel run to completion; then evaluate the property's condition on the final real state and print exactly one line, REPLAY_RESULT=reproduced when the condition is violated or REPLAY_RESULT=diverged when it holds \u2014 chosen by an if/else or a conditional expression in the harness, never a fixed string.
|
|
27996
|
+
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.
|
|
27997
|
+
6. Nothing you run can reach the network. Do not try.
|
|
27998
|
+
7. Keep it under about eight shell calls. Output tokens are time.
|
|
27999
|
+
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).
|
|
27745
28000
|
|
|
27746
28001
|
"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.`;
|
|
27747
28002
|
var REPLAY_SCHEMA = {
|
|
@@ -27844,6 +28099,8 @@ function printedResult(outputTexts) {
|
|
|
27844
28099
|
return last;
|
|
27845
28100
|
}
|
|
27846
28101
|
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";
|
|
28102
|
+
var FIXED_VERDICT = 'the harness printed a fixed verdict \u2014 it never contains the "diverged" outcome, so the result line was not computed from the real run';
|
|
28103
|
+
var CONTAINER_RETRY_BACKOFF_MS = [1e3, 3e3];
|
|
27847
28104
|
function judgeReplay(brief, parsed) {
|
|
27848
28105
|
const texts = parsed.shellTexts;
|
|
27849
28106
|
const primary = brief.sources[0];
|
|
@@ -27878,6 +28135,12 @@ function judgeReplay(brief, parsed) {
|
|
|
27878
28135
|
if (claimed === "reproduced" && printedResult(parsed.outputTexts) !== "reproduced") {
|
|
27879
28136
|
return { claimed, verdict: "unverified", reason: parsed.claim.reason, downgrade: RESULT_MISSING, verbatim };
|
|
27880
28137
|
}
|
|
28138
|
+
if (claimed === "reproduced" && !texts.some((t) => t.includes("diverged"))) {
|
|
28139
|
+
return { claimed, verdict: "unverified", reason: parsed.claim.reason, downgrade: FIXED_VERDICT, verbatim };
|
|
28140
|
+
}
|
|
28141
|
+
if (claimed === "reproduced" && primary && harnessInvokes(texts, primary.symbol, /\.py$/.test(primary.file)) === false) {
|
|
28142
|
+
return { claimed, verdict: "unverified", reason: parsed.claim.reason, downgrade: NOT_INVOKED, verbatim };
|
|
28143
|
+
}
|
|
27881
28144
|
return { claimed, verdict: claimed, reason: parsed.claim.reason, downgrade: null, verbatim };
|
|
27882
28145
|
}
|
|
27883
28146
|
async function runReplayInContainer(brief, opts) {
|
|
@@ -27902,24 +28165,36 @@ async function runReplayInContainer(brief, opts) {
|
|
|
27902
28165
|
error
|
|
27903
28166
|
});
|
|
27904
28167
|
let containerId = null;
|
|
27905
|
-
|
|
27906
|
-
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
27919
|
-
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
28168
|
+
for (let attempt = 1; ; attempt++) {
|
|
28169
|
+
let failure;
|
|
28170
|
+
let retryable = false;
|
|
28171
|
+
try {
|
|
28172
|
+
const res = await fetchImpl(`${baseUrl}/containers`, {
|
|
28173
|
+
method: "POST",
|
|
28174
|
+
headers,
|
|
28175
|
+
body: JSON.stringify({
|
|
28176
|
+
name: `verity-prove-replay-${brief.model}-${brief.property.id}`.slice(0, 64),
|
|
28177
|
+
memory_limit: "1g",
|
|
28178
|
+
network_policy: { type: "disabled" },
|
|
28179
|
+
expires_after: { anchor: "last_active_at", minutes: 1 }
|
|
28180
|
+
}),
|
|
28181
|
+
signal: AbortSignal.timeout(3e4)
|
|
28182
|
+
});
|
|
28183
|
+
if (res.ok) {
|
|
28184
|
+
const data = await res.json();
|
|
28185
|
+
if (typeof data.id !== "string" || !data.id.startsWith("cntr_")) return empty("container create returned no id", null);
|
|
28186
|
+
containerId = data.id;
|
|
28187
|
+
break;
|
|
28188
|
+
}
|
|
28189
|
+
failure = `container create failed: HTTP ${res.status} ${(await res.text().catch(() => "")).slice(0, 200)}`;
|
|
28190
|
+
retryable = res.status === 429 || res.status >= 500;
|
|
28191
|
+
} catch (err) {
|
|
28192
|
+
failure = `container create failed: ${err.message}`;
|
|
28193
|
+
retryable = true;
|
|
28194
|
+
}
|
|
28195
|
+
const backoffMs = (opts.containerRetryBackoffMs ?? CONTAINER_RETRY_BACKOFF_MS)[attempt - 1];
|
|
28196
|
+
if (!retryable || backoffMs === void 0) return empty(failure, null);
|
|
28197
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
27923
28198
|
}
|
|
27924
28199
|
try {
|
|
27925
28200
|
const tools = [{ type: "shell", environment: { type: "container_reference", container_id: containerId } }];
|
|
@@ -27983,11 +28258,17 @@ async function runReplayInContainer(brief, opts) {
|
|
|
27983
28258
|
last = { parsed: merged, judged: judged2, status: r.data.status, incomplete: r.data.incomplete_details?.reason };
|
|
27984
28259
|
if (judged2.verdict !== "unverified" || !judged2.downgrade) break;
|
|
27985
28260
|
const v = judged2.verbatim;
|
|
27986
|
-
const
|
|
28261
|
+
const fixedVerdict = judged2.downgrade === FIXED_VERDICT;
|
|
28262
|
+
const notInvoked = judged2.downgrade === NOT_INVOKED;
|
|
28263
|
+
const resultMissing = judged2.downgrade === RESULT_MISSING || fixedVerdict || notInvoked;
|
|
27987
28264
|
if (!resultMissing && (v.total === 0 || v.ratio >= (v.basis === "symbol" ? VERBATIM_THRESHOLD_SYMBOL : VERBATIM_THRESHOLD_FILE))) break;
|
|
27988
28265
|
const region = brief.sources[0];
|
|
27989
28266
|
const what = region ? `\`${region.symbol}\`${region.lines ? ` (lines ${region.lines.from}\u2013${region.lines.to})` : ""} of ${region.file}` : "the function under test";
|
|
27990
|
-
userContent =
|
|
28267
|
+
userContent = notInvoked ? `Your previous attempt in this same container was NOT accepted: the harness never calls the function under test \u2014 the trace was replayed on the stubs, not on the code. The files you created are still present. Drive the trace by CALLING the real function (its environment moves come from the stubs and the driver), run it, and answer with what it printed.
|
|
28268
|
+
|
|
28269
|
+
` + renderBriefInput(brief) : fixedVerdict ? `Your previous attempt in this same container was NOT accepted: the harness prints a FIXED verdict \u2014 it never contains the "diverged" outcome, so the result line was not computed from the run. The files you created are still present. Make the harness evaluate the property's condition on the final real state and print REPLAY_RESULT=reproduced or REPLAY_RESULT=diverged from an if/else, run it, and answer with what it printed.
|
|
28270
|
+
|
|
28271
|
+
` + renderBriefInput(brief) : resultMissing ? `Your previous attempt in this same container was NOT accepted: you answered "reproduced" but no shell output contained the line REPLAY_RESULT=reproduced. The files you created are still present. Run the harness again so that it prints REPLAY_RESULT=reproduced or REPLAY_RESULT=diverged as the last line of its own stdout, then answer with what it printed.
|
|
27991
28272
|
|
|
27992
28273
|
` + renderBriefInput(brief) : `Your previous attempt in this same container was NOT accepted: only ${v.covered} of the ${v.total} significant lines of the real ${v.basis} appeared verbatim in what you wrote` + (last.parsed.claim ? ` (you answered "${last.parsed.claim.verdict}")` : "") + `. The files you created are still present. Rewrite the harness so that ${what} is copied in FULL, line for line from the numbered source below, with helpers, imports and variables it needs stubbed OUTSIDE the copied lines. Then run it again and answer.
|
|
27993
28274
|
|
|
@@ -29011,9 +29292,96 @@ function writeResult(root, report) {
|
|
|
29011
29292
|
return rel;
|
|
29012
29293
|
}
|
|
29013
29294
|
|
|
29295
|
+
// src/lib/prove/merge.ts
|
|
29296
|
+
function isCrashTrace(trace) {
|
|
29297
|
+
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));
|
|
29298
|
+
}
|
|
29299
|
+
function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
29300
|
+
if (!outcome) return { blocking: 0 };
|
|
29301
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
29302
|
+
const pushed = new Set(pushedFiles.map(norm));
|
|
29303
|
+
const blocking = outcome.findings.filter((f) => f.verdict === "reproduced" && !f.waived && pushed.has(norm(f.file)));
|
|
29304
|
+
if (blocking.length === 0) return { blocking: 0 };
|
|
29305
|
+
const existing = Array.isArray(response.findings) ? response.findings : [];
|
|
29306
|
+
response.findings = [
|
|
29307
|
+
...existing,
|
|
29308
|
+
...blocking.map((f) => ({
|
|
29309
|
+
pattern_id: "formal-counterexample",
|
|
29310
|
+
severity: "high",
|
|
29311
|
+
scope: "change",
|
|
29312
|
+
file: f.file,
|
|
29313
|
+
line: outcome.target?.lines?.from ?? null,
|
|
29314
|
+
// Worded as the VIOLATION: the property text is what should hold, and
|
|
29315
|
+
// as a title it read backwards (e2e, 2026-09-26). A trace that needs a
|
|
29316
|
+
// crash between two writes is labelled: reproducing it means injecting
|
|
29317
|
+
// the crash, so it is a crash-consistency finding, not a race.
|
|
29318
|
+
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`,
|
|
29319
|
+
description: terminalText(f.reason ?? "", 2e3),
|
|
29320
|
+
fix: { description: `Trace: ${terminalText(f.trace.join(" \u2192 ") || "(initial state)", 2e3)}. Replay: ${terminalText(f.reason ?? "reproduced in the container", 2e3)}` },
|
|
29321
|
+
source: "formal"
|
|
29322
|
+
}))
|
|
29323
|
+
];
|
|
29324
|
+
if (response.gate_decision !== "FAIL") response.gate_decision = "FAIL";
|
|
29325
|
+
return { blocking: blocking.length };
|
|
29326
|
+
}
|
|
29327
|
+
|
|
29328
|
+
// src/lib/prove/scalpel.ts
|
|
29329
|
+
var import_node_crypto15 = require("node:crypto");
|
|
29330
|
+
var ARITHMETIC = /Math\.(?:round|floor|ceil|trunc)\(|\bround\(|\bdivmod\(|[\w)\]]\s*(?:\*|\/(?!\/)|%)\s*[\w(]/;
|
|
29331
|
+
function functionHazard(numbered) {
|
|
29332
|
+
const sites = effectSites(numbered);
|
|
29333
|
+
const body = numbered.split("\n").map((l) => l.replace(/^\s*\d+\| ?/, "")).filter((l) => !/^\s*(\/\/|#|\*|\/\*)/.test(l)).join("\n");
|
|
29334
|
+
return {
|
|
29335
|
+
effects: sites.filter((s) => s.includes("[effect]")).length,
|
|
29336
|
+
control: sites.filter((s) => s.includes("[control]")).length,
|
|
29337
|
+
arithmetic: ARITHMETIC.test(body)
|
|
29338
|
+
};
|
|
29339
|
+
}
|
|
29340
|
+
function hasHazard(h) {
|
|
29341
|
+
if (h.effects >= 2) return { ok: true, why: `${h.effects} effects that can interleave` };
|
|
29342
|
+
if (h.effects === 1 && h.control >= 1) return { ok: true, why: "an effect inside a loop or a try" };
|
|
29343
|
+
if (h.effects === 0 && h.arithmetic) return { ok: true, why: "pure arithmetic" };
|
|
29344
|
+
if (h.effects === 1) return { ok: false, why: "one effect and no loop or error path: nothing to interleave" };
|
|
29345
|
+
return { ok: false, why: "no effects and no arithmetic" };
|
|
29346
|
+
}
|
|
29347
|
+
var VERIFIED_FILE = `${PROOFS_DIR}/.results/.verified.json`;
|
|
29348
|
+
var VERIFIED_TTL_DAYS = 30;
|
|
29349
|
+
var LOG_LINE = /^\s*(?:console\.\w+|logger\.\w+|log\.\w+|logging\.\w+|print|debug|trace)\s*\(/;
|
|
29350
|
+
function semanticFingerprint(numbered) {
|
|
29351
|
+
const kept = [];
|
|
29352
|
+
for (const raw of numbered.split("\n")) {
|
|
29353
|
+
const line = raw.replace(/^\s*\d+\| ?/, "");
|
|
29354
|
+
const t = line.trim();
|
|
29355
|
+
if (!t || t.startsWith("//") || t.startsWith("#") || t.startsWith("*") || t.startsWith("/*")) continue;
|
|
29356
|
+
if (LOG_LINE.test(t)) continue;
|
|
29357
|
+
kept.push(t.replace(/\s+/g, " "));
|
|
29358
|
+
}
|
|
29359
|
+
return (0, import_node_crypto15.createHash)("sha256").update(kept.join("\n")).digest("hex");
|
|
29360
|
+
}
|
|
29361
|
+
function readVerified(root, now = Date.now()) {
|
|
29362
|
+
const read = readRegularFileInRoot(root, VERIFIED_FILE, 256 * 1024);
|
|
29363
|
+
if (!read.ok) return [];
|
|
29364
|
+
try {
|
|
29365
|
+
const all = JSON.parse(read.content);
|
|
29366
|
+
if (!Array.isArray(all)) return [];
|
|
29367
|
+
const cutoff = now - VERIFIED_TTL_DAYS * 864e5;
|
|
29368
|
+
return all.filter((r) => !!r && typeof r === "object" && typeof r.file === "string" && typeof r.symbol === "string" && typeof r.fingerprint === "string" && typeof r.at === "string" && Date.parse(r.at) >= cutoff);
|
|
29369
|
+
} catch {
|
|
29370
|
+
return [];
|
|
29371
|
+
}
|
|
29372
|
+
}
|
|
29373
|
+
function findVerified(root, file, symbol, fingerprint, now = Date.now()) {
|
|
29374
|
+
return readVerified(root, now).find((r) => r.file === file && r.symbol === symbol && r.fingerprint === fingerprint) ?? null;
|
|
29375
|
+
}
|
|
29376
|
+
function recordVerified(root, rec, now = Date.now()) {
|
|
29377
|
+
const others = readVerified(root, now).filter((r) => !(r.file === rec.file && r.symbol === rec.symbol));
|
|
29378
|
+
writeFileInRootNoFollow(root, VERIFIED_FILE, JSON.stringify([...others, rec].slice(-500)) + "\n");
|
|
29379
|
+
}
|
|
29380
|
+
|
|
29014
29381
|
// src/lib/prove/auto.ts
|
|
29015
29382
|
var AUTO_MODELS_DIR = `${MODELS_DIR}/.auto`;
|
|
29016
|
-
var DEFAULT_MIN_SCORE = 0.
|
|
29383
|
+
var DEFAULT_MIN_SCORE = 0.7;
|
|
29384
|
+
var MAX_MODEL_FUNCTION_LINES = 300;
|
|
29017
29385
|
var LARGE_PUSH_FILES = 25;
|
|
29018
29386
|
var DAILY_MODEL_CAP = 5;
|
|
29019
29387
|
var PUSH_COST_CAP_USD = 1;
|
|
@@ -29021,14 +29389,14 @@ var DAILY_COST_CAP_USD = 5;
|
|
|
29021
29389
|
var AUTO_LEDGER = `${PROOFS_DIR}/.results/.auto-ledger.json`;
|
|
29022
29390
|
function isEligible(s, largePush) {
|
|
29023
29391
|
const r = s.risk;
|
|
29392
|
+
const effects = s.effects ?? 0;
|
|
29024
29393
|
const critical = r.money >= 1 || r.transaction >= 1 || r.auth >= 3;
|
|
29025
|
-
const shapes = r.check_then_act
|
|
29026
|
-
|
|
29027
|
-
if (
|
|
29394
|
+
const shapes = r.check_then_act + r.concurrency + r.cache_or_idempotency + r.retry >= 2;
|
|
29395
|
+
if (largePush) return critical && effects >= 2 ? { ok: true, why: "domain-critical effects in a large push" } : { ok: false, why: "large push: only domain-critical code with effects qualifies" };
|
|
29396
|
+
if (effects >= 2) return { ok: true, why: `${effects} effect lines` };
|
|
29397
|
+
if (shapes) return { ok: true, why: "risky shape (check-then-act, concurrency, cache, retry)" };
|
|
29028
29398
|
if (critical) return { ok: true, why: "domain-critical" };
|
|
29029
|
-
|
|
29030
|
-
if (!shapes) return { ok: false, why: "no risky shape (check-then-act, concurrency, cache, retry)" };
|
|
29031
|
-
return { ok: false, why: "not thorny enough (needs 2 fix commits or 6 commits)" };
|
|
29399
|
+
return { ok: false, why: "no effects, no risky shape, not domain-critical" };
|
|
29032
29400
|
}
|
|
29033
29401
|
function intersects(a, ranges) {
|
|
29034
29402
|
return ranges.some((b) => a.from <= b.to && b.from <= a.to);
|
|
@@ -29104,7 +29472,7 @@ function workingTreeReader(root) {
|
|
|
29104
29472
|
};
|
|
29105
29473
|
}
|
|
29106
29474
|
function sha256Of(content) {
|
|
29107
|
-
return (0,
|
|
29475
|
+
return (0, import_node_crypto16.createHash)("sha256").update(content).digest("hex");
|
|
29108
29476
|
}
|
|
29109
29477
|
function defaultDeps(opts) {
|
|
29110
29478
|
const env = opts.env ?? process.env;
|
|
@@ -29233,6 +29601,23 @@ async function runFormalPhase(input) {
|
|
|
29233
29601
|
continue;
|
|
29234
29602
|
}
|
|
29235
29603
|
}
|
|
29604
|
+
if (d.lines) {
|
|
29605
|
+
const size = d.lines.to - d.lines.from + 1;
|
|
29606
|
+
if (size > MAX_MODEL_FUNCTION_LINES) {
|
|
29607
|
+
note("too-large", `${c.ai.symbol}: ${size} lines (at most ${MAX_MODEL_FUNCTION_LINES} are modelled)`);
|
|
29608
|
+
continue;
|
|
29609
|
+
}
|
|
29610
|
+
const hazard = hasHazard(functionHazard(d.numbered));
|
|
29611
|
+
if (!hazard.ok) {
|
|
29612
|
+
note("no-hazard", `${c.ai.symbol}: ${hazard.why}`);
|
|
29613
|
+
continue;
|
|
29614
|
+
}
|
|
29615
|
+
const seen = findVerified(root, c.file, c.ai.symbol, semanticFingerprint(d.numbered));
|
|
29616
|
+
if (seen) {
|
|
29617
|
+
note("verified", `${c.ai.symbol}: model ${seen.model} held on ${seen.at.slice(0, 10)} (${seen.properties.join(", ")}); this push changed only logging, comments or whitespace in it`);
|
|
29618
|
+
continue;
|
|
29619
|
+
}
|
|
29620
|
+
}
|
|
29236
29621
|
chosen = { top: c, content: read.content, lines: d.lines, numbered: d.numbered };
|
|
29237
29622
|
break;
|
|
29238
29623
|
}
|
|
@@ -29299,7 +29684,8 @@ async function runFormalPhase(input) {
|
|
|
29299
29684
|
writeResult(root, report);
|
|
29300
29685
|
} catch {
|
|
29301
29686
|
}
|
|
29302
|
-
const
|
|
29687
|
+
const isCrash = (p) => isCrashTrace(p.trace ?? []) || /\bcrash/i.test(p.desc);
|
|
29688
|
+
const cexes = report.properties.filter((p) => p.result === "counterexample").map((p, i) => ({ p, i })).sort((a, b) => Number(isCrash(a.p)) - Number(isCrash(b.p)) || a.i - b.i).map(({ p }) => p);
|
|
29303
29689
|
const baseOf = (p) => ({ property: p.id, desc: p.desc, kind: p.kind, trace: p.trace ?? [], file: top.file, symbol: top.ai.symbol });
|
|
29304
29690
|
const maxReplays = input.maxReplays ?? MAX_REPLAYS;
|
|
29305
29691
|
let skipWhy = null;
|
|
@@ -29325,7 +29711,7 @@ async function runFormalPhase(input) {
|
|
|
29325
29711
|
const r = await deps.replay(root, report, p, modelSource.ok ? modelSource.content : "", budget);
|
|
29326
29712
|
spend(r);
|
|
29327
29713
|
if (!stoppedEarly) byId.set(p.id, r);
|
|
29328
|
-
if (r.verdict === "reproduced" && !stoppedEarly) {
|
|
29714
|
+
if (r.verdict === "reproduced" && !stoppedEarly && !isCrash(p)) {
|
|
29329
29715
|
stoppedEarly = true;
|
|
29330
29716
|
resolve8();
|
|
29331
29717
|
}
|
|
@@ -29354,6 +29740,20 @@ async function runFormalPhase(input) {
|
|
|
29354
29740
|
...reproduced.map((f) => ({ file: f.file, symbol: f.symbol, sha256, property: f.property, desc: f.desc, kind: f.kind, trace: f.trace, model: modelName, reason: f.reason, ...lines ? { lines } : {}, at: now }))
|
|
29355
29741
|
]);
|
|
29356
29742
|
}
|
|
29743
|
+
const clean2 = findings.every((f) => f.verdict === "diverged");
|
|
29744
|
+
if (clean2 && lines && !cancelled()) {
|
|
29745
|
+
try {
|
|
29746
|
+
recordVerified(root, {
|
|
29747
|
+
file: top.file,
|
|
29748
|
+
symbol: top.ai.symbol,
|
|
29749
|
+
fingerprint: semanticFingerprint(numbered),
|
|
29750
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29751
|
+
model: modelName,
|
|
29752
|
+
properties: report.properties.map((p) => `${p.id} ${p.result === "counterexample" ? "diverged" : p.result}`)
|
|
29753
|
+
});
|
|
29754
|
+
} catch {
|
|
29755
|
+
}
|
|
29756
|
+
}
|
|
29357
29757
|
return done({
|
|
29358
29758
|
status: "checked",
|
|
29359
29759
|
target,
|
|
@@ -29478,39 +29878,6 @@ function describeFormalOutcome(o) {
|
|
|
29478
29878
|
return `formal: ${o.model?.name ?? "?"} on ${o.target?.file ?? "?"}#${o.target?.symbol ?? "?"} (${o.model?.rounds ?? 1} round${o.model?.rounds === 1 ? "" : "s"}) \xB7 ${parts.join(" \xB7 ")} \xB7 ${Math.round(o.durationMs / 1e3)} s${cost}${cut}`;
|
|
29479
29879
|
}
|
|
29480
29880
|
|
|
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
|
-
}
|
|
29485
|
-
function mergeFormalFindings(response, outcome, pushedFiles) {
|
|
29486
|
-
if (!outcome) return { blocking: 0 };
|
|
29487
|
-
const norm = (p) => p.replace(/\\/g, "/");
|
|
29488
|
-
const pushed = new Set(pushedFiles.map(norm));
|
|
29489
|
-
const blocking = outcome.findings.filter((f) => f.verdict === "reproduced" && !f.waived && pushed.has(norm(f.file)));
|
|
29490
|
-
if (blocking.length === 0) return { blocking: 0 };
|
|
29491
|
-
const existing = Array.isArray(response.findings) ? response.findings : [];
|
|
29492
|
-
response.findings = [
|
|
29493
|
-
...existing,
|
|
29494
|
-
...blocking.map((f) => ({
|
|
29495
|
-
pattern_id: "formal-counterexample",
|
|
29496
|
-
severity: "high",
|
|
29497
|
-
scope: "change",
|
|
29498
|
-
file: f.file,
|
|
29499
|
-
line: outcome.target?.lines?.from ?? null,
|
|
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`,
|
|
29505
|
-
description: terminalText(f.reason ?? "", 2e3),
|
|
29506
|
-
fix: { description: `Trace: ${terminalText(f.trace.join(" \u2192 ") || "(initial state)", 2e3)}. Replay: ${terminalText(f.reason ?? "reproduced in the container", 2e3)}` },
|
|
29507
|
-
source: "formal"
|
|
29508
|
-
}))
|
|
29509
|
-
];
|
|
29510
|
-
if (response.gate_decision !== "FAIL") response.gate_decision = "FAIL";
|
|
29511
|
-
return { blocking: blocking.length };
|
|
29512
|
-
}
|
|
29513
|
-
|
|
29514
29881
|
// src/commands/guard.ts
|
|
29515
29882
|
var EXCERPT_SOURCE_MAX_BYTES = 2 * 1024 * 1024;
|
|
29516
29883
|
var GUARD_BLOCK_CAP = 2;
|
|
@@ -30232,7 +30599,7 @@ function registerIgnoreCommand(program2) {
|
|
|
30232
30599
|
}
|
|
30233
30600
|
|
|
30234
30601
|
// src/commands/waive.ts
|
|
30235
|
-
var
|
|
30602
|
+
var import_node_crypto17 = require("node:crypto");
|
|
30236
30603
|
var import_node_fs57 = require("node:fs");
|
|
30237
30604
|
function registerWaiveCommand(program2) {
|
|
30238
30605
|
program2.command("waive <pattern-id>").description("Record an accepted-risk disposition for an open finding (voids when the file changes)").option("--file <path>", "File the finding is anchored to, REPO-RELATIVE (recommended \u2014 narrows the waive)").requiredOption("--reason <text>", "The human disposition this records (reviewer finding, ADR, \u2026)").action(async (patternId, opts) => {
|
|
@@ -30262,7 +30629,7 @@ function registerWaiveCommand(program2) {
|
|
|
30262
30629
|
if (opts.file) {
|
|
30263
30630
|
body.file = opts.file;
|
|
30264
30631
|
try {
|
|
30265
|
-
body.file_sha256 = (0,
|
|
30632
|
+
body.file_sha256 = (0, import_node_crypto17.createHash)("sha256").update((0, import_node_fs57.readFileSync)(opts.file)).digest("hex");
|
|
30266
30633
|
} catch {
|
|
30267
30634
|
printError(`Cannot read ${opts.file} \u2014 run from the repo root, or omit --file to waive by pattern.`);
|
|
30268
30635
|
process.exit(1);
|
|
@@ -31280,7 +31647,7 @@ async function promptMultiSelect(question, choices, fallback) {
|
|
|
31280
31647
|
|
|
31281
31648
|
// src/lib/prove/lean-install.ts
|
|
31282
31649
|
var import_node_fs60 = require("node:fs");
|
|
31283
|
-
var
|
|
31650
|
+
var import_node_crypto18 = require("node:crypto");
|
|
31284
31651
|
var import_node_child_process18 = require("node:child_process");
|
|
31285
31652
|
var import_node_os10 = require("node:os");
|
|
31286
31653
|
var import_node_path44 = require("node:path");
|
|
@@ -31302,7 +31669,7 @@ function expectedSha256(asset, version = pinnedVersion()) {
|
|
|
31302
31669
|
}
|
|
31303
31670
|
function sha256File(path) {
|
|
31304
31671
|
return new Promise((resolve8, reject) => {
|
|
31305
|
-
const hash = (0,
|
|
31672
|
+
const hash = (0, import_node_crypto18.createHash)("sha256");
|
|
31306
31673
|
(0, import_node_fs60.createReadStream)(path).on("data", (chunk) => hash.update(chunk)).on("end", () => resolve8(hash.digest("hex"))).on("error", reject);
|
|
31307
31674
|
});
|
|
31308
31675
|
}
|
|
@@ -32346,7 +32713,7 @@ function registerInitCommand(program2) {
|
|
|
32346
32713
|
...telemetryChoice ? { telemetry: telemetryChoice } : {},
|
|
32347
32714
|
init: {
|
|
32348
32715
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32349
|
-
cli_version: true ? "0.33.3-experimental.
|
|
32716
|
+
cli_version: true ? "0.33.3-experimental.b6d47c6" : "dev"
|
|
32350
32717
|
}
|
|
32351
32718
|
});
|
|
32352
32719
|
} catch (err) {
|
|
@@ -33808,8 +34175,8 @@ function registerLeanCommand(program2) {
|
|
|
33808
34175
|
}
|
|
33809
34176
|
|
|
33810
34177
|
// src/cli.ts
|
|
33811
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.
|
|
33812
|
-
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.
|
|
34178
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.b6d47c6").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) => {
|
|
34179
|
+
installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.b6d47c6");
|
|
33813
34180
|
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
33814
34181
|
try {
|
|
33815
34182
|
await foldLegacyLocalCredential();
|
package/package.json
CHANGED