@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.
Files changed (2) hide show
  1. package/bin/verity.js +492 -125
  2. 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.86b5b6f" : "dev";
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 import_node_crypto15 = require("node:crypto");
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 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.
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.
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.`;
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 RULES = `Write ONE Lean 4 model of the code below, in the style of the exemplar, that could expose a real bug. Rules:
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 6 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 is finite (a few hundred states at most).
27530
- - Actions: an inductive with a \`ToString\` instance naming the REAL operation (the function or event), so a trace reads like a scenario. Include the actions that can interleave in reality: a retry, a second call, a concurrent writer, a redelivered event, a crash between two writes.
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 := 6 }\` or less. No other \`#eval\`, no \`IO\`, no \`sorry\`, no attributes, no macros, no \`partial\`. Under 120 lines.
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 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").
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 SCHEMA = {
27540
- type: "object",
27541
- additionalProperties: false,
27542
- properties: {
27543
- name: { type: "string" },
27544
- lean_source: { type: "string" },
27545
- summary: { type: "string" }
27546
- },
27547
- required: ["name", "lean_source", "summary"]
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
- parts.push("");
27556
- parts.push("```");
27557
- parts.push(t.numbered);
27558
- parts.push("```");
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: `${RULES}
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 (write files with heredocs). 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.
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 (e.g. a unique index, an updated_at check, a file that exists or not). If you must stub something the trace depends on, say so in the reason.
27740
- 3. Drive the actions in the trace's order, controlling interleavings by hand (resolve promises in the order the trace says). Print the observable state after each action and a final line: REPLAY_RESULT=reproduced or REPLAY_RESULT=diverged.
27741
- 4. Detect runtimes first (node --version, python3 --version, deno --version). For TypeScript with no TypeScript runner available, prefer "node --experimental-strip-types"; if that is unavailable, remove type annotations ONLY on lines that are not part of the function under test's logic, and say so.
27742
- 5. Nothing you run can reach the network. Do not try.
27743
- 6. Keep it under about a dozen shell calls. Output tokens are time.
27744
- 7. 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).
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
- try {
27906
- const res = await fetchImpl(`${baseUrl}/containers`, {
27907
- method: "POST",
27908
- headers,
27909
- body: JSON.stringify({
27910
- name: `verity-prove-replay-${brief.model}-${brief.property.id}`.slice(0, 64),
27911
- memory_limit: "1g",
27912
- network_policy: { type: "disabled" },
27913
- expires_after: { anchor: "last_active_at", minutes: 1 }
27914
- }),
27915
- signal: AbortSignal.timeout(3e4)
27916
- });
27917
- if (!res.ok) return empty(`container create failed: HTTP ${res.status} ${(await res.text().catch(() => "")).slice(0, 200)}`, null);
27918
- const data = await res.json();
27919
- if (typeof data.id !== "string" || !data.id.startsWith("cntr_")) return empty("container create returned no id", null);
27920
- containerId = data.id;
27921
- } catch (err) {
27922
- return empty(`container create failed: ${err.message}`, null);
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 resultMissing = judged2.downgrade === RESULT_MISSING;
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 = 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.
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.75;
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 >= 2 || r.concurrency >= 2 || r.cache_or_idempotency >= 2 || r.retry >= 2;
29026
- const thorny = s.fixCommits >= 2 || s.commits >= 6;
29027
- if (largePush) return critical ? { ok: true, why: "domain-critical in a large push" } : { ok: false, why: "large push: only domain-critical code qualifies" };
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
- if (shapes && thorny) return { ok: true, why: "risky shape and thorny history" };
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, import_node_crypto15.createHash)("sha256").update(content).digest("hex");
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 cexes = report.properties.filter((p) => p.result === "counterexample");
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 import_node_crypto16 = require("node:crypto");
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, import_node_crypto16.createHash)("sha256").update((0, import_node_fs57.readFileSync)(opts.file)).digest("hex");
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 import_node_crypto17 = require("node:crypto");
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, import_node_crypto17.createHash)("sha256");
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.86b5b6f" : "dev"
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.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");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.33.3-experimental.86b5b6f",
3
+ "version": "0.33.3-experimental.b6d47c6",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://verity.md",