@codacy/verity-cli 0.33.3-experimental.46f0715 → 0.33.3-experimental.53cd005

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 +271 -38
  2. package/package.json +1 -1
package/bin/verity.js CHANGED
@@ -26771,7 +26771,7 @@ function channelSilence(input) {
26771
26771
  // src/lib/cli-version.ts
26772
26772
  function cliVersion() {
26773
26773
  try {
26774
- return true ? "0.33.3-experimental.46f0715" : "dev";
26774
+ return true ? "0.33.3-experimental.53cd005" : "dev";
26775
26775
  } catch {
26776
26776
  return "dev";
26777
26777
  }
@@ -28487,6 +28487,13 @@ function definitionsIn(root, spec) {
28487
28487
  } else if (bound && p.type === "pair") {
28488
28488
  const key = p.childForFieldName("key");
28489
28489
  if (key && (key.type === "property_identifier" || key.type === "identifier")) add(key.text, kind2, p, n, p);
28490
+ } else if (p.type === "arguments" && moduleCall(p.parent)) {
28491
+ const call = p.parent;
28492
+ const route = p.namedChildren[0];
28493
+ const quoted = route && route.id !== n.id && route.type === "string" ? unquote2(route.text) : null;
28494
+ const path = quoted && /^[\w/:.{}*-]{1,60}$/.test(quoted) ? quoted : null;
28495
+ const callee = call.childForFieldName("function").text;
28496
+ add(path ? `${callee} ${path}` : callee, kind2, call.parent, n, call);
28490
28497
  }
28491
28498
  }
28492
28499
  for (const c of n.namedChildren) visit(c);
@@ -28494,6 +28501,10 @@ function definitionsIn(root, spec) {
28494
28501
  visit(root);
28495
28502
  return out2;
28496
28503
  }
28504
+ function moduleCall(call) {
28505
+ const f = call?.type === "call_expression" ? call.childForFieldName("function") : null;
28506
+ return !!f && (f.type === "identifier" || f.type === "member_expression") && f.text.length <= 40 && !f.text.includes("\n") && call.parent?.type === "expression_statement" && call.parent.parent?.type === "program";
28507
+ }
28497
28508
  function unquote2(s) {
28498
28509
  const body2 = s.replace(/^[rRbBuUfF]{0,2}(?=["'`])/, "");
28499
28510
  const q3 = body2.startsWith('"""') || body2.startsWith("'''");
@@ -28562,6 +28573,25 @@ function factsOf(def, spec, defs, path) {
28562
28573
  const loops = [];
28563
28574
  const tries = [];
28564
28575
  const locks = [];
28576
+ const bindings = {};
28577
+ const bind = (n) => {
28578
+ let name2 = null;
28579
+ let value = null;
28580
+ if (n.type === "variable_declarator") {
28581
+ name2 = n.childForFieldName("name");
28582
+ value = n.childForFieldName("value");
28583
+ } else if (n.type === "assignment" && spec.id === "python") {
28584
+ name2 = n.childForFieldName("left");
28585
+ value = n.childForFieldName("right");
28586
+ } else if (n.type === "short_var_declaration") {
28587
+ const l = n.childForFieldName("left"), r = n.childForFieldName("right");
28588
+ if (l?.namedChildren.length === 1 && r?.namedChildren.length === 1) {
28589
+ name2 = l.namedChildren[0];
28590
+ value = r.namedChildren[0];
28591
+ }
28592
+ }
28593
+ if (name2?.type === "identifier" && value && Object.keys(bindings).length < 40 && !(name2.text in bindings)) bindings[name2.text] = cap(value.text, TEXT_CAP);
28594
+ };
28565
28595
  const throwsIn = (n) => {
28566
28596
  if (spec.throws.has(n.type)) return true;
28567
28597
  if (defs.has(n.id) || spec.functionValues.has(n.type)) return false;
@@ -28571,6 +28601,7 @@ function factsOf(def, spec, defs, path) {
28571
28601
  const anonymousMember = n.parent?.type === "class_body" && n.parent.parent?.type === "object_creation_expression";
28572
28602
  if (n.id !== def.fn.id && !anonymousMember && (defs.has(n.id) || spec.classes.has(n.type) || spec.definitions[n.type])) return;
28573
28603
  const t = n.type;
28604
+ bind(n);
28574
28605
  const down = (c, next = ctx) => walk2(c, next);
28575
28606
  if (spec.calls.has(t)) {
28576
28607
  const { receiver, name: name2, args: args2 } = callParts(n, spec);
@@ -28640,7 +28671,8 @@ function factsOf(def, spec, defs, path) {
28640
28671
  loops,
28641
28672
  tries,
28642
28673
  locks: [...new Set(locks)].sort((a, b) => a - b),
28643
- hasError: def.last.hasError
28674
+ hasError: def.last.hasError,
28675
+ ...Object.keys(bindings).length ? { bindings } : {}
28644
28676
  };
28645
28677
  }
28646
28678
  function endOf(def, spec) {
@@ -28676,6 +28708,54 @@ function outlineOf(rt, path, content, deadlineAt) {
28676
28708
  tree.delete();
28677
28709
  }
28678
28710
  }
28711
+ var BLOCKS = /* @__PURE__ */ new Set(["statement_block", "block", "switch_body"]);
28712
+ function regionOf(rt, path, content, fn, changed, maxLines, deadlineAt) {
28713
+ const started = Date.now();
28714
+ const lang = languageOf2(path);
28715
+ if (!lang) return { source: "fallback", reason: "unsupported-language", elapsedMs: 0 };
28716
+ const inside2 = changed.filter((r) => r.from <= fn.end.line && fn.start.line <= r.to);
28717
+ if (inside2.length === 0) return { source: "fallback", reason: "not-in-function", elapsedMs: 0 };
28718
+ const from = Math.max(fn.start.line, Math.min(...inside2.map((r) => r.from)));
28719
+ const to = Math.min(fn.end.line, Math.max(...inside2.map((r) => r.to)));
28720
+ if (to - from + 1 > maxLines) return { source: "fallback", reason: "change-too-wide", elapsedMs: 0 };
28721
+ if (Buffer.byteLength(content) > MAX_PARSE_BYTES) return { source: "fallback", reason: "too-large", elapsedMs: 0 };
28722
+ const tree = parseWithin(rt, lang, content, deadlineAt);
28723
+ if (!tree) return { source: "fallback", reason: "deadline", elapsedMs: Date.now() - started };
28724
+ try {
28725
+ const lines = content.split("\n");
28726
+ let n = tree.rootNode.descendantForPosition({ row: from - 1, column: 0 }, { row: to - 1, column: (lines[to - 1] ?? "").length });
28727
+ let block = null;
28728
+ for (; n && line(n) >= fn.start.line; n = n.parent) {
28729
+ if (BLOCKS.has(n.type) && n.endPosition.row + 1 - line(n) + 1 > maxLines) {
28730
+ block = n;
28731
+ break;
28732
+ }
28733
+ }
28734
+ if (!block) return { source: "fallback", reason: "no-block", elapsedMs: Date.now() - started };
28735
+ const stmts = block.namedChildren.filter((c) => c.type !== "comment");
28736
+ const end = (c) => c.endPosition.row + 1;
28737
+ let lo = stmts.findIndex((c) => end(c) >= from);
28738
+ let hi = stmts.length - 1 - [...stmts].reverse().findIndex((c) => line(c) <= to);
28739
+ if (lo < 0 || hi < lo) return { source: "fallback", reason: "no-block", elapsedMs: Date.now() - started };
28740
+ const fits = (a, b) => end(stmts[b]) - line(stmts[a]) + 1 <= maxLines;
28741
+ if (!fits(lo, hi)) return { source: "fallback", reason: "change-too-wide", elapsedMs: Date.now() - started };
28742
+ for (let grew = true; grew; ) {
28743
+ grew = false;
28744
+ if (lo > 0 && fits(lo - 1, hi)) {
28745
+ lo--;
28746
+ grew = true;
28747
+ }
28748
+ if (hi < stmts.length - 1 && fits(lo, hi + 1)) {
28749
+ hi++;
28750
+ grew = true;
28751
+ }
28752
+ }
28753
+ const holder = block.parent && block.parent.type !== "program" ? block.parent : block;
28754
+ return { source: "tree-sitter", value: { from: line(stmts[lo]), to: end(stmts[hi]), within: cap(holder.text.split("\n")[0].trim(), 60) }, elapsedMs: Date.now() - started };
28755
+ } finally {
28756
+ tree.delete();
28757
+ }
28758
+ }
28679
28759
 
28680
28760
  // src/lib/code/effects.ts
28681
28761
  function literalName(literal) {
@@ -28696,7 +28776,18 @@ function argResource(kind, arg) {
28696
28776
  const n = literalName(arg.literal);
28697
28777
  return n ? { kind, name: n.name, confidence: n.confidence } : { kind, name: null, confidence: "unknown" };
28698
28778
  }
28699
- return SIMPLE_EXPRESSION.test(arg.text) && STABLE_EXPRESSION(arg.text) ? { kind, name: arg.text, confidence: "expression" } : { kind, name: null, confidence: "unknown" };
28779
+ if (SIMPLE_EXPRESSION.test(arg.text) && STABLE_EXPRESSION(arg.text)) return { kind, name: arg.text, confidence: "expression" };
28780
+ const keyed = keyedCall(arg.text);
28781
+ return keyed ? { kind, name: keyed, confidence: "expression" } : { kind, name: null, confidence: "unknown" };
28782
+ }
28783
+ function keyedCall(text) {
28784
+ const t = text.trim();
28785
+ const open = t.indexOf("(");
28786
+ if (open <= 0 || !t.endsWith(")")) return null;
28787
+ const callee = t.slice(0, open);
28788
+ if (!/^[A-Za-z_$][\w$.]*$/.test(callee)) return null;
28789
+ const constant = /\b[A-Z][A-Z0-9_]{2,}\b/.exec(t.slice(open + 1, -1));
28790
+ return constant ? `${callee}(${constant[0]})` : null;
28700
28791
  }
28701
28792
  function snake(s) {
28702
28793
  return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
@@ -28932,10 +29023,18 @@ function memory(c, fn, state) {
28932
29023
  const op = MEMORY_WRITE.has(c.name) ? "write" : MEMORY_READ.has(c.name) ? "read" : null;
28933
29024
  return op ? { op, resource: { kind: "memory", name: `${fn.path}#${c.receiver}`, confidence: "literal" }, rule: "memory.module" } : null;
28934
29025
  }
29026
+ function resolveLocal(a, bindings) {
29027
+ if (a.literal !== null || !/^[A-Za-z_$][\w$]*$/.test(a.text)) return a;
29028
+ const init2 = bindings[a.text];
29029
+ if (!init2) return a;
29030
+ const quoted = /^(['"`])([^'"`$]*)\1$/.exec(init2.trim());
29031
+ return { ...a, text: init2, literal: quoted ? quoted[2] : null };
29032
+ }
28935
29033
  function directEffects(fn, lang, state) {
28936
29034
  const out2 = [];
28937
29035
  const seen = /* @__PURE__ */ new Set();
28938
- for (const c of fn.calls) {
29036
+ for (const raw of fn.calls) {
29037
+ const c = fn.bindings ? { ...raw, args: raw.args.map((a) => resolveLocal(a, fn.bindings)) } : raw;
28939
29038
  let m = memory(c, fn, state);
28940
29039
  if (!m) {
28941
29040
  for (const rule of RULES) if (m = rule(c, fn, lang)) break;
@@ -28993,7 +29092,11 @@ function sameResource(a, b) {
28993
29092
  function searchTerms(r) {
28994
29093
  if (!r.name || r.confidence === "unknown") return [];
28995
29094
  if (r.kind === "service" || r.kind === "memory") return [];
28996
- if (r.confidence === "expression") return [r.name.endsWith("()") ? r.name.slice(0, -1) : r.name];
29095
+ if (r.confidence === "expression") {
29096
+ const keyed = /^[A-Za-z_$][\w$.]*\(([A-Z][A-Z0-9_]+)\)$/.exec(r.name);
29097
+ if (keyed) return [keyed[1]];
29098
+ return [r.name.endsWith("()") ? r.name.slice(0, -1) : r.name];
29099
+ }
28997
29100
  if (r.kind === "table" && r.confidence === "inferred") {
28998
29101
  const one = tableKey(r.name);
28999
29102
  const plural = (w) => /[^aeiou]y$/.test(w) ? `${w.slice(0, -1)}ies` : /(?:s|x|ch|sh)$/.test(w) ? `${w}es` : `${w}s`;
@@ -29978,6 +30081,13 @@ function makeCodeMap(rt) {
29978
30081
  return { source: "fallback", reason: `error: ${String(e?.message ?? e).slice(0, 120)}`, elapsedMs: Date.now() - started };
29979
30082
  }
29980
30083
  },
30084
+ regionAt(path, content, target, changed, maxLines, deadlineAt = Date.now() + DEFAULT_PARSE_MS) {
30085
+ try {
30086
+ return regionOf(rt, path, content, target, changed, maxLines, deadlineAt);
30087
+ } catch (e) {
30088
+ return { source: "fallback", reason: `error: ${String(e?.message ?? e).slice(0, 120)}`, elapsedMs: 0 };
30089
+ }
30090
+ },
29981
30091
  functionContaining(path, content, line2, deadlineAt) {
29982
30092
  return pick(outline(path, content, deadlineAt), (fns) => fns.filter((f) => f.start.line <= line2 && line2 <= f.end.line).sort((a, b) => a.end.line - a.start.line - (b.end.line - b.start.line))[0], "no-enclosing-function");
29983
30093
  }
@@ -33206,6 +33316,10 @@ var STRUCTURAL_CATALOGUE = [
33206
33316
  "check_then_act_is_atomic",
33207
33317
  "no_double_effect"
33208
33318
  ];
33319
+ function combineScore(c) {
33320
+ const f = (x) => Math.max(0.01, Math.min(1, x));
33321
+ return Math.round(Math.pow(f(c.hazard), 0.45) * Math.pow(f(c.critical), 0.35) * Math.pow(f(c.error_prone), 0.2) * 1e3) / 1e3;
33322
+ }
33209
33323
  function deterministicOrder(candidates2) {
33210
33324
  return candidates2.map((c, i2) => ({ ...c, rank: i2 + 1 }));
33211
33325
  }
@@ -33215,37 +33329,51 @@ var deterministicRanker = {
33215
33329
  return { ok: true, ranker: "deterministic", ranked: deterministicOrder(candidates2) };
33216
33330
  }
33217
33331
  };
33218
- function excerpt(root, file, bytes, readSource, changed, codeMap) {
33332
+ function excerpt(root, file, bytes, readSource, changed, codeMap, codeSource) {
33219
33333
  const read2 = readSource ? readSource(file, EXCERPT_SOURCE_CAP_BYTES) : readRegularFileInRoot(root, file, EXCERPT_SOURCE_CAP_BYTES);
33220
33334
  if (!read2.ok) return "";
33221
33335
  if (changed && changed.length > 0) {
33222
- const touched = codeMap ? touchedFunctions(codeMap, file, read2.content, changed) : "";
33336
+ const touched = codeMap ? touchedFunctions(codeMap, file, read2.content, changed, { source: codeSource, fixSubjects: (from, to) => fixSubjects(root, file, from, to) }) : "";
33223
33337
  return touched + changedExcerpt(read2.content, changed, bytes - touched.length);
33224
33338
  }
33225
33339
  return read2.content.length > bytes ? read2.content.slice(0, bytes) + "\n\u2026 (truncated)" : read2.content;
33226
33340
  }
33227
- function touchedFunctions(codeMap, file, content, changed) {
33341
+ function touchedFunctions(codeMap, file, content, changed, evidence2 = {}) {
33228
33342
  const r = codeMap.outline(file, content);
33229
33343
  if (r.source !== "tree-sitter") return "";
33230
33344
  const size = (f) => f.end.line - f.start.line;
33231
33345
  const names2 = [];
33346
+ const fixes = [];
33232
33347
  for (const range of changed) {
33233
33348
  const around = r.value.filter((f) => f.start.line <= range.to && range.from <= f.end.line).sort((a, b) => size(a) - size(b));
33234
33349
  const hit = around[0];
33235
33350
  const outer = hit ? r.value.filter((f) => f !== hit && f.start.line <= hit.start.line && hit.end.line <= f.end.line).sort((a, b) => size(a) - size(b))[0] : void 0;
33236
- const label2 = hit ? `${hit.qualified} (lines ${hit.start.line}\u2013${hit.end.line}${outer ? `, inside ${outer.qualified}` : ""}${hit ? sharedState(codeMap, file, content, hit) : ""})` : null;
33237
- if (label2 && !names2.includes(label2)) names2.push(label2);
33351
+ const label2 = hit ? `${hit.qualified} (lines ${hit.start.line}\u2013${hit.end.line}${outer ? `, inside ${outer.qualified}` : ""}${hit ? sharedState(codeMap, file, content, hit, evidence2.source) : ""})` : null;
33352
+ if (label2 && !names2.includes(label2)) {
33353
+ names2.push(label2);
33354
+ for (const s of hit && evidence2.fixSubjects ? evidence2.fixSubjects(hit.start.line, hit.end.line) : []) if (!fixes.includes(s)) fixes.push(s);
33355
+ }
33238
33356
  }
33239
- return names2.length > 0 ? `Functions this push changed: ${names2.slice(0, 8).join(", ")}
33357
+ if (names2.length === 0) return "";
33358
+ const history = fixes.length > 0 ? `Fix commits that changed these lines: ${fixes.slice(0, 4).map((s) => `"${s}"`).join("; ")}
33240
33359
  ` : "";
33360
+ return `Functions this push changed: ${names2.slice(0, 8).join(", ")}
33361
+ ${history}`;
33241
33362
  }
33242
- function sharedState(codeMap, file, content, fn) {
33243
- const r = codeMap.access(file, content, fn);
33363
+ function fixSubjects(root, file, from, to) {
33364
+ const r = runGitSync(root, ["log", "-n", "40", "--no-patch", "--format=%s", "-L", `${from},${to}:${file}`], { timeoutMs: 2e3, maxBuffer: 1 << 20 });
33365
+ if (!r.ok) return [];
33366
+ return [...new Set(r.stdout.split("\n").map((s) => s.trim()).filter((s) => s && FIX_SUBJECT.test(s)))].slice(0, 4).map((s) => s.slice(0, 120));
33367
+ }
33368
+ function sharedState(codeMap, file, content, fn, source2) {
33369
+ const r = codeMap.access(file, content, fn, source2 ? { source: source2, deadlineAt: Date.now() + 800 } : void 0);
33244
33370
  if (r.source !== "tree-sitter") return "";
33245
33371
  const notes = [];
33246
- const others = r.value.accessors.slice(0, 2).map((a) => a.fn.qualified);
33372
+ const others = r.value.accessors.slice(0, 2).map((a) => a.fn.path === file ? a.fn.qualified : `${a.fn.qualified} in ${a.fn.path}`);
33247
33373
  if (others.length) notes.push(`shares state with ${others.join(" and ")}`);
33248
33374
  if (r.value.signals.checkThenAct.length) notes.push(`reads then writes ${r.value.signals.checkThenAct[0].split(" ")[0]}`);
33375
+ const kinds = [...new Set(r.value.effects.filter((e) => e.resource.name).map((e) => `${e.op} ${e.resource.kind}:${e.resource.name}`))].slice(0, 4);
33376
+ if (kinds.length) notes.push(`effects: ${kinds.join(", ")}`);
33249
33377
  return notes.length ? `; ${notes.join("; ")}` : "";
33250
33378
  }
33251
33379
  function changedExcerpt(content, changed, bytes, context = 30) {
@@ -33285,25 +33413,27 @@ var LUNA_SCHEMA = {
33285
33413
  additionalProperties: false,
33286
33414
  properties: {
33287
33415
  file: { type: "string" },
33288
- score: { type: "number" },
33416
+ critical: { type: "number" },
33417
+ hazard: { type: "number" },
33418
+ error_prone: { type: "number" },
33289
33419
  reasons: { type: "array", items: { type: "string" } },
33290
33420
  symbol: { type: "string" },
33291
33421
  properties: { type: "array", items: { type: "string", enum: [...STRUCTURAL_CATALOGUE] } }
33292
33422
  },
33293
- required: ["file", "score", "reasons", "symbol", "properties"]
33423
+ required: ["file", "critical", "hazard", "error_prone", "reasons", "symbol", "properties"]
33294
33424
  }
33295
33425
  }
33296
33426
  },
33297
33427
  required: ["items"]
33298
33428
  };
33299
33429
  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.
33300
- 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:
33301
- - 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);
33302
- - 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;
33303
- - 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.
33304
- 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.
33305
- 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.
33306
- 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. An excerpt may open with "Functions this push changed:", from a parser: when it says a changed function "shares state with" another, judge the PAIR \u2014 a function that alone is a single write or an invalidation is error-prone when the other reads and repopulates, or also writes, the same state (a cache invalidated here and refilled there, two writers of one ledger or file); score it as that pair would be scored, but name ONE symbol \u2014 the changed function, never both ("a/b", "a and b" name nothing). "reads then writes" names a check-then-act the parser saw with no transaction or lock around it.`;
33430
+ For each file you get its git-history signals (commits, fix commits), risk keyword counts and an excerpt. Pick the ONE function here most worth a model, and rate it 0-1 on each of three criteria, separately \u2014 the service combines them (a model is worth the minute only when all three hold):
33431
+ - 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 or at most once \u2014 duplicated records, nodes, charges, emails or seeds count, and so does state that two processes can leave inconsistent);
33432
+ - hazard: something a model can explore \u2014 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;
33433
+ - error_prone: fix commits in its history (the strongest evidence: this code has already been wrong), high churn, several branches or error paths, a concurrency primitive \u2014 a hazard nobody sees by reading the function once.
33434
+ Rate a criterion LOW (below 0.5) when it 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.
33435
+ 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 \u2014 its hazard, what a mistake costs, its history \u2014 never about the change. Return every file you were given, exactly once.
33436
+ 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. An excerpt may open with "Functions this push changed:", from a parser: when it says a changed function "shares state with" another, judge the PAIR \u2014 a function that alone is a single write or an invalidation is error-prone when the other reads and repopulates, or also writes, the same state (a cache invalidated here and refilled there, two writers of one ledger or file); score it as that pair would be scored, but name ONE symbol \u2014 the changed function, never both ("a/b", "a and b" name nothing). "reads then writes" names a check-then-act the parser saw with no transaction or lock around it. "effects" lists the state the function reads and writes (a table, a file, a cache key, a queue, an outside service). "Fix commits that changed these lines" quotes the history of THIS code: each one is evidence it has been wrong before (error_prone), and its subject often says what a mistake cost (critical) \u2014 a subject about duplicates, lost writes, double charges or stuck jobs is a costly mistake that already happened.`;
33307
33437
  var lunaRanker = {
33308
33438
  name: "luna",
33309
33439
  async rank(candidates2, ctx) {
@@ -33358,7 +33488,12 @@ var lunaRanker = {
33358
33488
  const text = (data.output ?? []).flatMap((o) => o.content ?? []).find((c) => c.type === "output_text")?.text;
33359
33489
  if (!text) return { ok: false, ranker: "luna", error: "no output_text in response", ranked: fallback };
33360
33490
  const parsed = JSON.parse(text);
33361
- return { ok: true, ranker: "luna", ranked: applyAnnotations(candidates2, parsed.items) };
33491
+ const items = parsed.items.map((i2) => {
33492
+ const legacy = i2.critical === void 0 && i2.hazard === void 0 && i2.error_prone === void 0;
33493
+ const criteria = legacy ? { critical: Number(i2.score) || 0, hazard: Number(i2.score) || 0, error_prone: Number(i2.score) || 0 } : { critical: Number(i2.critical) || 0, hazard: Number(i2.hazard) || 0, error_prone: Number(i2.error_prone) || 0 };
33494
+ return { ...i2, criteria, score: legacy ? Number(i2.score) || 0 : combineScore(criteria) };
33495
+ });
33496
+ return { ok: true, ranker: "luna", ranked: applyAnnotations(candidates2, items) };
33362
33497
  } catch (err2) {
33363
33498
  const msg = err2.name === "AbortError" ? "timed out" : err2.message;
33364
33499
  return { ok: false, ranker: "luna", error: msg, ranked: fallback };
@@ -33382,7 +33517,7 @@ async function rankViaServer(candidates2, ctx, auth2) {
33382
33517
  fix_commits: c.fixCommits,
33383
33518
  risk: c.risk,
33384
33519
  lines: c.lines,
33385
- excerpt: excerpt(ctx.root, c.file, excerptBytes, ctx.readSource, ctx.changedLines?.get(c.file), ctx.codeMap),
33520
+ excerpt: excerpt(ctx.root, c.file, excerptBytes, ctx.readSource, ctx.changedLines?.get(c.file), ctx.codeMap, ctx.codeSource),
33386
33521
  ...ctx.changedLines?.get(c.file)?.length ? { changed_lines: ctx.changedLines.get(c.file).slice(0, 50) } : {}
33387
33522
  })),
33388
33523
  ...ctx.hint ? { hint: ctx.hint.slice(0, 500) } : {}
@@ -33402,6 +33537,11 @@ function applyAnnotations(candidates2, items) {
33402
33537
  const properties = (a.properties ?? []).filter((p) => STRUCTURAL_CATALOGUE.includes(p));
33403
33538
  const score = Math.max(0, Math.min(1, Number(a.score) || 0));
33404
33539
  const ai = { score, reasons: (a.reasons ?? []).slice(0, 3), properties };
33540
+ if (a.criteria && typeof a.criteria === "object") {
33541
+ const u = (v) => Math.max(0, Math.min(1, Number(v) || 0));
33542
+ ai.criteria = { critical: u(a.criteria.critical), hazard: u(a.criteria.hazard), error_prone: u(a.criteria.error_prone) };
33543
+ }
33544
+ if (a.reasked) ai.reasked = true;
33405
33545
  if (a.symbol && a.symbol.trim()) ai.symbol = a.symbol.trim();
33406
33546
  return { ...c, rank: 0, ai };
33407
33547
  });
@@ -35728,6 +35868,7 @@ function contextHazard(ctx) {
35728
35868
  var AUTO_MODELS_DIR = `${MODELS_DIR}/.auto`;
35729
35869
  var DEFAULT_MIN_SCORE = 0.7;
35730
35870
  var MAX_MODEL_FUNCTION_LINES = 300;
35871
+ var REGION_MIN_SCORE = 0.8;
35731
35872
  var REPLAYABLE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|py)$/;
35732
35873
  var LARGE_PUSH_FILES = 25;
35733
35874
  var DAILY_MODEL_CAP = 5;
@@ -35824,7 +35965,7 @@ function sha256Of(content) {
35824
35965
  function defaultDeps(opts) {
35825
35966
  const env = opts.env ?? process.env;
35826
35967
  return {
35827
- rank: (candidates2, root, changedLines) => lunaRanker.rank(candidates2, { root, auth: opts.auth, env, fetchImpl: opts.fetchImpl, readSource: opts.readSource, codeMap: opts.codeMap, ...changedLines ? { changedLines } : {} }),
35968
+ rank: (candidates2, root, changedLines) => lunaRanker.rank(candidates2, { root, auth: opts.auth, env, fetchImpl: opts.fetchImpl, readSource: opts.readSource, codeMap: opts.codeMap, ...opts.codeSource ? { codeSource: opts.codeSource } : {}, ...changedLines ? { changedLines } : {} }),
35828
35969
  // The push phase writes with the FAST tier; `prove generate` asks for DEEP (decision 56).
35829
35970
  generate: (target, previous, budgetMs) => generateModel(target, { auth: opts.auth, apiKey: opts.apiKey, env, fetchImpl: opts.fetchImpl, previous, tier: opts.tier ?? "fast", ...budgetMs ? { budgetMs } : {} }),
35830
35971
  check: (root, modelFile) => checkModelAsync(opts.toolchain, root, modelFile),
@@ -35841,8 +35982,10 @@ async function runFormalPhase(input) {
35841
35982
  const progress = input.progress ?? {};
35842
35983
  progress.usage = usage;
35843
35984
  progress.step = "select";
35985
+ let candidates2;
35844
35986
  const done = (partial) => ({
35845
35987
  findings: [],
35988
+ ...candidates2 ? { candidates: candidates2 } : {},
35846
35989
  ...partial,
35847
35990
  usage,
35848
35991
  durationMs: deps.now() - started
@@ -35907,17 +36050,42 @@ async function runFormalPhase(input) {
35907
36050
  const signals = collectRiskSignals(root, { paths: files, limit: 10 });
35908
36051
  if (signals.length === 0) return done({ status: "skipped", skip: "no-source-files" });
35909
36052
  const largePush = signals.length >= LARGE_PUSH_FILES || files.length >= LARGE_PUSH_FILES * 2;
35910
- const eligible = signals.filter((s) => isEligible(s, largePush).ok).slice(0, 5);
36053
+ const mapEligible = (file) => {
36054
+ const changed = input.changedLines?.get(file);
36055
+ if (!deps.codeMap || largePush || !changed?.length) return false;
36056
+ const read2 = readSrc(file, 96 * 1024);
36057
+ if (!read2.ok) return false;
36058
+ const outline = deps.codeMap.outline(file, read2.content);
36059
+ if (outline.source !== "tree-sitter") return false;
36060
+ return outline.value.some((f) => changed.some((r) => f.start.line <= r.to && r.from <= f.end.line) && (() => {
36061
+ const a = deps.codeMap.access(file, read2.content, f);
36062
+ return a.source === "tree-sitter" && a.value.effects.length >= 2;
36063
+ })());
36064
+ };
36065
+ const eligible = signals.filter((s) => isEligible(s, largePush).ok || mapEligible(s.file)).slice(0, 5);
35911
36066
  if (eligible.length === 0) {
35912
36067
  const best = signals[0];
35913
36068
  return done({ status: "skipped", skip: "not-eligible", detail: `${best.file}: ${isEligible(best, largePush).why}` });
35914
36069
  }
35915
36070
  const ranked = await deps.rank(eligible, root, input.changedLines);
35916
36071
  if (ranked.ok) spend(ranked);
36072
+ candidates2 = ranked.ranked.slice(0, 5).map((c) => ({ file: c.file, symbol: c.ai?.symbol ?? null, score: c.ai?.score ?? null, criteria: c.ai?.criteria ?? null, reasked: !!c.ai?.reasked, reasons: (c.ai?.reasons ?? []).map((r) => r.slice(0, 200)) }));
35917
36073
  if (cancelled()) return done({ status: "skipped", skip: "deadline", detail: "cut off after ranking" });
35918
36074
  const minScore = input.minScore ?? DEFAULT_MIN_SCORE;
35919
- const qualified = ranked.ranked.filter((c) => c.ai && c.ai.score >= minScore);
35920
- if (qualified.length === 0) return done({ status: "skipped", skip: "no-candidate", detail: ranked.ok ? `best score ${Math.round((ranked.ranked[0]?.ai?.score ?? 0) * 100)}% of ${eligible.length} eligible` : `rank unavailable: ${ranked.error ?? ""}` });
36075
+ let qualified = ranked.ranked.filter((c) => c.ai && c.ai.score >= minScore);
36076
+ let floor = false;
36077
+ if (qualified.length === 0 && ranked.ok && deps.codeMap && input.changedLines) {
36078
+ const pick = floorCandidate(ranked.ranked, input.changedLines, deps, readSrc, root);
36079
+ if (pick) {
36080
+ qualified = [pick];
36081
+ floor = true;
36082
+ }
36083
+ }
36084
+ if (qualified.length === 0) {
36085
+ const best = ranked.ranked[0];
36086
+ const why2 = best?.ai ? `${best.ai.symbol ?? best.file}${best.ai.criteria ? ` (critical ${best.ai.criteria.critical}, hazard ${best.ai.criteria.hazard}, error-prone ${best.ai.criteria.error_prone})` : ""}${best.ai.reasons[0] ? ` \u2014 ${best.ai.reasons[0].slice(0, 160)}` : ""}` : "";
36087
+ return done({ status: "skipped", skip: "no-candidate", detail: ranked.ok ? `best score ${Math.round((best?.ai?.score ?? 0) * 100)}% of ${eligible.length} eligible${why2 ? `: ${why2}` : ""}` : `rank unavailable: ${ranked.error ?? ""}` });
36088
+ }
35921
36089
  let chosen = null;
35922
36090
  let why = null;
35923
36091
  const note = (skip, detail) => {
@@ -35934,7 +36102,18 @@ async function runFormalPhase(input) {
35934
36102
  note("read-failed", `${c.file}: ${read2.reason}`);
35935
36103
  continue;
35936
36104
  }
35937
- const d = describeTarget(read2.content, c.ai.symbol, c.file, void 0, { codeMap: deps.codeMap, prefer: input.changedLines?.get(c.file) });
36105
+ let d = describeTarget(read2.content, c.ai.symbol, c.file, void 0, { codeMap: deps.codeMap, prefer: input.changedLines?.get(c.file) });
36106
+ if (!d.lines && deps.codeMap && input.changedLines?.get(c.file)?.length) {
36107
+ const only = changedFunction(deps.codeMap, c.file, read2.content, input.changedLines.get(c.file));
36108
+ if (only) {
36109
+ const again = describeTarget(read2.content, only, c.file, void 0, { codeMap: deps.codeMap, prefer: input.changedLines.get(c.file) });
36110
+ if (again.lines) {
36111
+ c.ai.reasons = [`the ranker named ${c.ai.symbol}, which is not a function here; the push changed ${only}`, ...c.ai.reasons].slice(0, 4);
36112
+ c.ai.symbol = only;
36113
+ d = again;
36114
+ }
36115
+ }
36116
+ }
35938
36117
  if (d.resolved && d.resolved !== c.ai.symbol) c.ai.symbol = d.resolved;
35939
36118
  if (input.changedLines) {
35940
36119
  const changed = input.changedLines.get(c.file) ?? [];
@@ -35954,13 +36133,30 @@ async function runFormalPhase(input) {
35954
36133
  if (d.lines) {
35955
36134
  const size = d.lines.to - d.lines.from + 1;
35956
36135
  if (size > MAX_MODEL_FUNCTION_LINES) {
35957
- note("too-large", `${c.ai.symbol}: ${size} lines (at most ${MAX_MODEL_FUNCTION_LINES} are modelled)`);
35958
- continue;
36136
+ const fn = deps.codeMap?.functionAt(c.file, read2.content, c.ai.symbol, input.changedLines?.get(c.file));
36137
+ const region = fn?.source === "tree-sitter" && input.changedLines?.get(c.file)?.length ? deps.codeMap.regionAt(c.file, read2.content, fn.value, input.changedLines.get(c.file), MAX_MODEL_FUNCTION_LINES) : null;
36138
+ if (region?.source !== "tree-sitter") {
36139
+ note("too-large", `${c.ai.symbol}: ${size} lines (at most ${MAX_MODEL_FUNCTION_LINES} are modelled)`);
36140
+ continue;
36141
+ }
36142
+ const { from, to, within } = region.value;
36143
+ const ctx = deps.codeMap ? targetContext({ codeMap: deps.codeMap, codeSource: deps.codeSource, readSource: readSrc, file: c.file, content: read2.content, symbol: c.ai.symbol, prefer: input.changedLines?.get(c.file) }) : null;
36144
+ const inRegion = (ctx?.signals.checkThenAct ?? []).some((x) => {
36145
+ const m = /\(L(\d+)→L(\d+)\)/.exec(x);
36146
+ return !!m && from <= Number(m[1]) && Number(m[2]) <= to;
36147
+ });
36148
+ if (c.ai.score < REGION_MIN_SCORE || !inRegion) {
36149
+ note("too-large", `${c.ai.symbol}: ${size} lines, and no read-then-write of state in lines ${from}\u2013${to} scored ${REGION_MIN_SCORE} or more`);
36150
+ continue;
36151
+ }
36152
+ d = { ...describeTarget(read2.content, c.ai.symbol, c.file, { from, to }), extent: "region" };
36153
+ c.ai.reasons = [`region: lines ${from}\u2013${to} of the ${size}-line ${c.ai.symbol}, in \`${within}\``, ...c.ai.reasons].slice(0, 4);
35959
36154
  }
35960
36155
  context = deps.codeMap ? targetContext({ codeMap: deps.codeMap, codeSource: deps.codeSource, readSource: readSrc, file: c.file, content: read2.content, symbol: c.ai.symbol, prefer: input.changedLines?.get(c.file) }) : null;
35961
36156
  const hazard = hasHazard(functionHazard(d.numbered));
35962
36157
  const shared = contextHazard(context);
35963
- if (!hazard.ok && !shared?.ok) {
36158
+ const judged = (c.ai.criteria?.hazard ?? 0) >= 0.8;
36159
+ if (!hazard.ok && !shared?.ok && !judged) {
35964
36160
  note("no-hazard", `${c.ai.symbol}: ${hazard.why}`);
35965
36161
  continue;
35966
36162
  }
@@ -35981,7 +36177,7 @@ async function runFormalPhase(input) {
35981
36177
  const top = chosen.top;
35982
36178
  const sha2562 = sha256Of(chosen.content);
35983
36179
  const { lines, numbered: numbered2 } = chosen;
35984
- const target = { file: top.file, symbol: top.ai.symbol, lines, score: top.ai.score, reasons: [...top.reasons, ...top.ai.reasons], extent: chosen.extent, ...chosen.context ? { context: chosen.context.stats } : {} };
36180
+ const target = { file: top.file, symbol: top.ai.symbol, lines, score: top.ai.score, reasons: [...top.reasons, ...top.ai.reasons], extent: chosen.extent, ...chosen.context ? { context: chosen.context.stats } : {}, ...floor ? { floor: true } : {} };
35985
36181
  progress.target = target;
35986
36182
  if (!REPLAYABLE.test(top.file)) return done({ status: "skipped", skip: "replay-unsupported", detail: `${top.file}: replay supports TS/JS and Python`, target });
35987
36183
  if (timeLeft() < GENERATE_RESERVE_MS) return done({ status: "skipped", skip: "not-enough-time", detail: `${Math.round(timeLeft() / 1e3)} s left before generation`, target });
@@ -36203,7 +36399,40 @@ async function prepareFormalPhase(root, env = process.env, auth2, readSource, co
36203
36399
  const build = await buildLibraryAsync(toolchain, root, 9e4);
36204
36400
  if (!build.ok) return { ok: false, skip: "library-build-failed", detail: build.stderr.split("\n").filter((l) => !l.startsWith("trace:")).join(" ").slice(0, 200) };
36205
36401
  const loaded = await loadCodeMap(env);
36206
- return { ok: true, deps: { ...defaultDeps({ auth: auth2, apiKey, env, toolchain, readSource, codeMap: loaded.ok ? loaded.map : null }), codeSource: codeSource ?? withoutPaths(worktreeSource(root), isDeniedPath) } };
36402
+ const source2 = codeSource ?? withoutPaths(worktreeSource(root), isDeniedPath);
36403
+ return { ok: true, deps: { ...defaultDeps({ auth: auth2, apiKey, env, toolchain, readSource, codeMap: loaded.ok ? loaded.map : null, codeSource: source2 }), codeSource: source2 } };
36404
+ }
36405
+ function changedFunction(codeMap, file, content, changed) {
36406
+ const outline = codeMap.outline(file, content);
36407
+ if (outline.source !== "tree-sitter") return null;
36408
+ const size = (f) => f.end.line - f.start.line;
36409
+ const touched = new Set(changed.map((r) => outline.value.filter((f) => f.start.line <= r.to && r.from <= f.end.line).sort((a, b) => size(a) - size(b))[0]?.qualified).filter(Boolean));
36410
+ return touched.size === 1 ? [...touched][0] : null;
36411
+ }
36412
+ function floorCandidate(ranked, changedLines, deps, readSrc, root) {
36413
+ for (const c of ranked.slice(0, 3)) {
36414
+ const changed = changedLines.get(c.file);
36415
+ if (!changed?.length || !deps.codeMap) continue;
36416
+ const read2 = readSrc(c.file, 96 * 1024);
36417
+ if (!read2.ok) continue;
36418
+ const outline = deps.codeMap.outline(c.file, read2.content);
36419
+ if (outline.source !== "tree-sitter") continue;
36420
+ const size = (f) => f.end.line - f.start.line;
36421
+ const touched = [...new Set(changed.map((r) => outline.value.filter((f) => f.start.line <= r.to && r.from <= f.end.line).sort((a, b) => size(a) - size(b))[0]).filter(Boolean))];
36422
+ if ((c.ai?.criteria?.hazard ?? 0) < 0.5) continue;
36423
+ for (const fn of touched.slice(0, 2)) {
36424
+ if (size(fn) + 1 > MAX_MODEL_FUNCTION_LINES) continue;
36425
+ const context = targetContext({ codeMap: deps.codeMap, codeSource: deps.codeSource, readSource: readSrc, file: c.file, content: read2.content, symbol: fn.qualified, prefer: changed });
36426
+ const checkThenAct = context?.signals.checkThenAct ?? [];
36427
+ if (checkThenAct.length === 0) continue;
36428
+ const writers = context?.signals.otherWriters ?? 0;
36429
+ const fixes = writers > 0 ? [] : fixSubjects(root, c.file, fn.start.line, fn.end.line);
36430
+ if (writers === 0 && fixes.length === 0) continue;
36431
+ const why = writers > 0 ? `floor: another function writes the same state (${context?.fields.coAccessors?.map((a) => a.symbol).join(", ") || "found by search"}) and it reads then writes ${checkThenAct[0].split(" ")[0]}` : `floor: fixed before ("${fixes[0]}") and it reads then writes ${checkThenAct[0].split(" ")[0]}`;
36432
+ return { ...c, ai: { score: c.ai?.score ?? 0, symbol: fn.qualified, reasons: [why, ...c.ai?.reasons ?? []].slice(0, 3), properties: c.ai?.properties ?? [], ...c.ai?.criteria ? { criteria: c.ai.criteria } : {} } };
36433
+ }
36434
+ }
36435
+ return null;
36207
36436
  }
36208
36437
  function runFormalPhaseWithin(input) {
36209
36438
  const left = input.deadlineAt - input.deps.now();
@@ -36772,6 +37001,10 @@ async function runGuard(opts, globals) {
36772
37001
  extent: formalOutcome.target?.extent ?? null,
36773
37002
  // The code map's context for generation: counts only (day 2).
36774
37003
  context: formalOutcome.target?.context ?? null,
37004
+ // Why this push was, or was not, modelled (VRT-260): the ranker's scores and criteria per candidate.
37005
+ candidates: (formalOutcome.candidates ?? []).map((c) => ({ file: c.file, symbol: c.symbol, score: c.score, criteria: c.criteria, reasked: c.reasked })),
37006
+ floor: formalOutcome.target?.floor ?? false,
37007
+ detail: formalOutcome.detail ?? null,
36775
37008
  rounds: formalOutcome.model?.rounds ?? null,
36776
37009
  properties: formalOutcome.properties?.map((p) => `${p.id}:${p.result}`) ?? [],
36777
37010
  findings: formalOutcome.findings.map((f) => `${f.property}:${f.verdict}`),
@@ -39119,7 +39352,7 @@ function registerInitCommand(program2) {
39119
39352
  ...telemetryChoice ? { telemetry: telemetryChoice } : {},
39120
39353
  init: {
39121
39354
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
39122
- cli_version: true ? "0.33.3-experimental.46f0715" : "dev"
39355
+ cli_version: true ? "0.33.3-experimental.53cd005" : "dev"
39123
39356
  }
39124
39357
  });
39125
39358
  } catch (err2) {
@@ -40581,8 +40814,8 @@ function registerLeanCommand(program2) {
40581
40814
  }
40582
40815
 
40583
40816
  // src/cli.ts
40584
- program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.46f0715").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) => {
40585
- installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.46f0715");
40817
+ program.name("verity").description("CLI for Verity quality gate service").version("0.33.3-experimental.53cd005").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) => {
40818
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.33.3-experimental.53cd005");
40586
40819
  setUserNamedServiceUrl(program.opts().serviceUrl);
40587
40820
  try {
40588
40821
  await foldLegacyLocalCredential();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.33.3-experimental.46f0715",
3
+ "version": "0.33.3-experimental.53cd005",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://verity.md",