@nudojs/core 1.13.0 → 1.14.1

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.
@@ -47,7 +47,7 @@ import {
47
47
  withEvalBudgetOverride,
48
48
  withExecPhi,
49
49
  withFnBudgetOverride
50
- } from "./chunk-A2PLG2QV.js";
50
+ } from "./chunk-JMOQEJTD.js";
51
51
 
52
52
  // src/algebra/directive-scan.ts
53
53
  function cleanDirectiveLine(raw, kind) {
@@ -622,6 +622,14 @@ function scanAssumeSegments(lines) {
622
622
  }
623
623
  return out;
624
624
  }
625
+ function scanInvariantSegments(lines) {
626
+ const out = [];
627
+ for (const line of lines) {
628
+ const m = line.match(/^@nudo:invariant\s+(.+)$/);
629
+ if (m) out.push(m[1].trim().replace(/\*\/$/, "").trim());
630
+ }
631
+ return out;
632
+ }
625
633
  function scanThrowsDecl(lines) {
626
634
  const kinds = /* @__PURE__ */ new Set();
627
635
  let any = false;
@@ -1516,6 +1524,35 @@ function extractAssumeSegs(source, fnName) {
1516
1524
  }
1517
1525
  return out;
1518
1526
  }
1527
+ var INVARIANT_SEG_REGEX = /^([A-Za-z_$][\w$]*)((?:\[\*\]|\.[A-Za-z_$][\w$]*)*)\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)$/;
1528
+ function extractInvariantSegs(source, fnName) {
1529
+ if (!stripStringsKeepComments(source).includes("@nudo:invariant")) return [];
1530
+ const out = [];
1531
+ for (const seg of scanInvariantSegments(fnDirectiveCommentLines(source, fnName))) {
1532
+ const m = seg.match(INVARIANT_SEG_REGEX);
1533
+ if (!m) {
1534
+ out.push({ kind: "bad", raw: seg });
1535
+ continue;
1536
+ }
1537
+ const pathStr = m[2] ?? "";
1538
+ const path = [];
1539
+ for (const pm of pathStr.matchAll(/\[\*\]|\.[A-Za-z_$][\w$]*/g)) {
1540
+ const t = pm[0];
1541
+ if (t === "[*]") path.push({ k: "idx" });
1542
+ else path.push({ k: "prop", name: t.slice(1) });
1543
+ }
1544
+ const relTxt = m[3];
1545
+ out.push({
1546
+ kind: "ok",
1547
+ name: m[1],
1548
+ path,
1549
+ rel: relTxt === ">=" ? "ge" : relTxt === "<=" ? "le" : relTxt === ">" ? "gt" : "lt",
1550
+ value: Number(m[4]),
1551
+ display: `${m[1]}${pathStr} ${relTxt} ${m[4]}`
1552
+ });
1553
+ }
1554
+ return out;
1555
+ }
1519
1556
  function fnTopLevelLocalNames(body) {
1520
1557
  const block = body;
1521
1558
  if (!block || block.type !== "BlockStatement" || !Array.isArray(block.body)) return [];
@@ -5533,6 +5570,395 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
5533
5570
  return out;
5534
5571
  }
5535
5572
 
5573
+ // src/algebra/check-report.ts
5574
+ function actionsForIssue(i) {
5575
+ const draft = {
5576
+ kind: "draft",
5577
+ command: "nudo contract --draft",
5578
+ label: "emit a sidecar draft you can edit"
5579
+ };
5580
+ switch (i.code) {
5581
+ case "nudo:constraint-violated":
5582
+ return [
5583
+ {
5584
+ kind: "callsite",
5585
+ label: "use a value satisfying the constraint",
5586
+ ...i.expected ? { hint: i.expected } : {}
5587
+ },
5588
+ {
5589
+ kind: "relax",
5590
+ label: "relax the precondition (edit *.nudo.js / @nudo:contract)",
5591
+ ...i.expected ? { hint: i.expected } : {}
5592
+ },
5593
+ draft
5594
+ ];
5595
+ case "nudo:assign-mismatch":
5596
+ return [
5597
+ {
5598
+ kind: "callsite",
5599
+ label: "align the assigned value with the existing shape",
5600
+ ...i.expected ? { hint: i.expected } : {}
5601
+ },
5602
+ draft
5603
+ ];
5604
+ case "nudo:entry-may-throw":
5605
+ return [
5606
+ {
5607
+ kind: "draft",
5608
+ command: "nudo contract --draft",
5609
+ label: "narrow the entry with a sidecar fn({ \u2026 }) param contract \u2014 most honest fix for helpers (also tightens the signature)",
5610
+ ...i.fn ? { hint: i.fn } : {}
5611
+ },
5612
+ {
5613
+ kind: "relax",
5614
+ label: "declare intentional fail-fast: @nudo:throws Error (or case `!! throws`)",
5615
+ ...i.fn ? { hint: i.fn } : {}
5616
+ },
5617
+ {
5618
+ kind: "info",
5619
+ label: "or refine / guard / try-catch the entry so it is total"
5620
+ },
5621
+ {
5622
+ kind: "ignore-throws",
5623
+ command: "nudo check --ignore-throws TypeError",
5624
+ label: "migration switch: ignore this throw class on L2"
5625
+ }
5626
+ ];
5627
+ case "nudo:unproven-return":
5628
+ return [
5629
+ {
5630
+ kind: "relax",
5631
+ label: `relax the return contract${i.fn ? ` of ${i.fn}` : ""} to the inferred surface (review the diff \u2014 not a silent green)`,
5632
+ ...i.expected ? { hint: i.expected } : {}
5633
+ },
5634
+ draft,
5635
+ {
5636
+ kind: "info",
5637
+ label: "or narrow the return expression / add a precondition so the bound is implied"
5638
+ }
5639
+ ];
5640
+ case "nudo:unknown-inference":
5641
+ case "nudo:opaque-result":
5642
+ return [
5643
+ { kind: "mock", label: "pin the native/unknown face with @nudo:mock" },
5644
+ { kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
5645
+ { kind: "info", label: "or add a call site / @nudo:case for evidence" }
5646
+ ];
5647
+ case "nudo:arg-opaque":
5648
+ case "nudo:constraint-unproven":
5649
+ return [
5650
+ { kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
5651
+ { kind: "info", label: "add a call site or @nudo:case" }
5652
+ ];
5653
+ case "nudo:fork-truncated":
5654
+ case "nudo:promise-micro-truncated":
5655
+ case "nudo:promise-micro-error":
5656
+ return [
5657
+ {
5658
+ kind: "info",
5659
+ label: "results widened to unknown \u2014 raise budget (@nudo:budget forks/calls/depth on the function) or narrow control flow"
5660
+ }
5661
+ ];
5662
+ case "nudo:loop-truncated":
5663
+ return [
5664
+ {
5665
+ kind: "info",
5666
+ label: "loop state widened \u2014 the exit condition stayed concrete-true past the iteration cap; make the bound data-dependent (abstract) or pin the result with @nudo:case / @nudo:contract"
5667
+ }
5668
+ ];
5669
+ case "nudo:recursion-truncated":
5670
+ return [
5671
+ {
5672
+ kind: "info",
5673
+ label: i.reason === "cycle" ? "call cycle: same abstract arguments already on the stack \u2014 raising @nudo:budget cannot lift it; make the arguments vary per recursion level or declare @nudo:contract" : i.reason === "depth" || i.reason === "calls" || i.reason === "budget" ? `results widened to unknown \u2014 raise budget (@nudo:budget ${i.reason === "depth" ? "depth" : i.reason === "calls" ? "calls" : "calls/depth"} on the function) or narrow the recursion` : "results widened to unknown \u2014 raise budget (@nudo:budget calls/depth on the function) or narrow control flow"
5674
+ }
5675
+ ];
5676
+ case "nudo:host-effect-blocked":
5677
+ return [
5678
+ { kind: "mock", label: "pin the host API with @nudo:mock / @nudo:env" },
5679
+ { kind: "info", label: "results widened to unknown \u2014 or feed the value in from a call site" }
5680
+ ];
5681
+ case "nudo:interface-drift":
5682
+ return [
5683
+ {
5684
+ kind: "emit",
5685
+ command: "nudo contract --emit",
5686
+ label: "refresh the @generated segment"
5687
+ }
5688
+ ];
5689
+ case "nudo:dual-entry":
5690
+ return [
5691
+ {
5692
+ kind: "info",
5693
+ label: "browser/node records do not cross files \u2014 analyze the entry you ship; mock or skip the other variant"
5694
+ }
5695
+ ];
5696
+ default:
5697
+ return i.suggestion ? [{ kind: "info", label: i.suggestion }] : [];
5698
+ }
5699
+ }
5700
+ function serializeCheckJson(r) {
5701
+ return {
5702
+ version: 1,
5703
+ file: r.file,
5704
+ ok: r.ok,
5705
+ summary: { ...r.summary },
5706
+ signatures: r.signatures.map((s) => ({
5707
+ name: s.name,
5708
+ params: [...s.params],
5709
+ ...s.paramTypes ? { paramTypes: [...s.paramTypes] } : {},
5710
+ display: s.display,
5711
+ detail: s.detail,
5712
+ conf: s.conf,
5713
+ abs: formatAbs(s.abs),
5714
+ ...s.throws ? { throws: s.throws } : {},
5715
+ ...s.entry ? { entry: true } : {}
5716
+ })),
5717
+ issues: r.issues.map((i) => {
5718
+ const acts = i.actions ?? actionsForIssue(i);
5719
+ return {
5720
+ severity: i.severity,
5721
+ code: i.code,
5722
+ message: i.message,
5723
+ ...i.fn !== void 0 ? { fn: i.fn } : {},
5724
+ ...i.line !== void 0 ? { line: i.line } : {},
5725
+ ...i.column !== void 0 ? { column: i.column } : {},
5726
+ ...i.actual !== void 0 ? { actual: i.actual } : {},
5727
+ ...i.expected !== void 0 ? { expected: i.expected } : {},
5728
+ ...i.suggestion !== void 0 ? { suggestion: i.suggestion } : {},
5729
+ ...i.reason !== void 0 ? { reason: i.reason } : {},
5730
+ ...acts.length > 0 ? { actions: acts } : {}
5731
+ };
5732
+ }),
5733
+ ...r.budget ? { budget: { ...r.budget } } : {}
5734
+ };
5735
+ }
5736
+ function serializeCheckJsonMulti(reports) {
5737
+ const summary = {
5738
+ errors: 0,
5739
+ warnings: 0,
5740
+ infos: 0,
5741
+ functions: 0,
5742
+ files: reports.length
5743
+ };
5744
+ let calls = 0;
5745
+ let forks = 0;
5746
+ let maxCalls = 0;
5747
+ let maxForks = 0;
5748
+ let callTruncated = false;
5749
+ let forkTruncated = false;
5750
+ let evalCalls = 0;
5751
+ let maxEvalCalls = 0;
5752
+ let evalCallTruncated = false;
5753
+ let cycleTruncated = false;
5754
+ let loopTruncated = false;
5755
+ const callReasons = /* @__PURE__ */ new Set();
5756
+ const evalReasons = /* @__PURE__ */ new Set();
5757
+ let depth = 0;
5758
+ let maxDepth = 0;
5759
+ let evalDepth = 0;
5760
+ let maxEvalDepth = 0;
5761
+ const reasons = /* @__PURE__ */ new Set();
5762
+ for (const r of reports) {
5763
+ summary.errors += r.summary.errors;
5764
+ summary.warnings += r.summary.warnings;
5765
+ summary.infos += r.summary.infos;
5766
+ summary.functions += r.summary.functions;
5767
+ if (r.budget) {
5768
+ calls += r.budget.calls;
5769
+ forks += r.budget.forks;
5770
+ maxCalls = Math.max(maxCalls, r.budget.maxCalls);
5771
+ maxForks = Math.max(maxForks, r.budget.maxForks);
5772
+ callTruncated = callTruncated || r.budget.callTruncated;
5773
+ forkTruncated = forkTruncated || r.budget.forkTruncated;
5774
+ evalCalls += r.budget.evalCalls ?? 0;
5775
+ maxEvalCalls = Math.max(maxEvalCalls, r.budget.maxEvalCalls ?? 0);
5776
+ evalCallTruncated = evalCallTruncated || (r.budget.evalCallTruncated ?? false);
5777
+ cycleTruncated = cycleTruncated || (r.budget.cycleTruncated ?? false);
5778
+ loopTruncated = loopTruncated || (r.budget.loopTruncated ?? false);
5779
+ for (const x of r.budget.callReasons ?? []) callReasons.add(x);
5780
+ for (const x of r.budget.evalReasons ?? []) evalReasons.add(x);
5781
+ depth = Math.max(depth, r.budget.depth ?? 0);
5782
+ maxDepth = Math.max(maxDepth, r.budget.maxDepth ?? 0);
5783
+ evalDepth = Math.max(evalDepth, r.budget.evalDepth ?? 0);
5784
+ maxEvalDepth = Math.max(maxEvalDepth, r.budget.maxEvalDepth ?? 0);
5785
+ for (const x of r.budget.reasons ?? []) reasons.add(x);
5786
+ }
5787
+ }
5788
+ const budgetTruncated = callTruncated || evalCallTruncated || forkTruncated || loopTruncated;
5789
+ return {
5790
+ version: 1,
5791
+ kind: "multi",
5792
+ ok: reports.every((r) => r.ok),
5793
+ summary: {
5794
+ ...summary,
5795
+ ...budgetTruncated ? { budgetTruncated: true } : {}
5796
+ },
5797
+ ...budgetTruncated || reports.some((r) => r.budget) ? {
5798
+ budget: {
5799
+ truncated: budgetTruncated,
5800
+ callTruncated,
5801
+ forkTruncated,
5802
+ calls,
5803
+ maxCalls,
5804
+ forks,
5805
+ maxForks,
5806
+ evalCalls,
5807
+ maxEvalCalls,
5808
+ evalCallTruncated,
5809
+ cycleTruncated,
5810
+ loopTruncated,
5811
+ callReasons: [...callReasons].sort(),
5812
+ depth,
5813
+ maxDepth,
5814
+ evalReasons: [...evalReasons].sort(),
5815
+ evalDepth,
5816
+ maxEvalDepth,
5817
+ reasons: [...reasons].sort()
5818
+ }
5819
+ } : {},
5820
+ reports
5821
+ };
5822
+ }
5823
+ var CONTRACT_FIX_CODES = /* @__PURE__ */ new Set([
5824
+ "nudo:constraint-violated",
5825
+ "nudo:assign-mismatch",
5826
+ "nudo:arg-structure",
5827
+ "nudo:interface-domain-exceeds",
5828
+ "nudo:entry-may-throw",
5829
+ "nudo:missing-slot"
5830
+ ]);
5831
+ function budgetAttributionSegs(b) {
5832
+ const segs = [];
5833
+ if ((b.callReasons ?? []).includes("calls")) segs.push(`calls ${b.calls}/${b.maxCalls}`);
5834
+ if ((b.callReasons ?? []).includes("depth")) segs.push(`call depth ${b.depth}/${b.maxDepth}`);
5835
+ if ((b.evalReasons ?? []).includes("calls")) {
5836
+ segs.push(`eval calls ${b.evalCalls}/${b.maxEvalCalls}`);
5837
+ }
5838
+ if ((b.evalReasons ?? []).includes("depth")) {
5839
+ segs.push(`eval depth ${b.evalDepth}/${b.maxEvalDepth}`);
5840
+ }
5841
+ if (b.forkTruncated) segs.push(`forks ${b.forks}/${b.maxForks}`);
5842
+ if (b.loopTruncated) segs.push("loop iterations");
5843
+ const cycleOnly = segs.length === 0;
5844
+ if (cycleOnly) segs.push("call cycle \u2014 not a budget limit");
5845
+ return { segs, cycleOnly };
5846
+ }
5847
+ function formatCheckReport(r, opts = {}) {
5848
+ const lines = [];
5849
+ lines.push(`nudo check ${r.file}`);
5850
+ lines.push(r.ok ? "OK" : "FAILED");
5851
+ lines.push(
5852
+ ` ${r.summary.errors} error \xB7 ${r.summary.warnings} warning \xB7 ${r.summary.infos} info \xB7 ${r.summary.functions} fn`
5853
+ );
5854
+ if (r.budget?.truncated) {
5855
+ const b = r.budget;
5856
+ const onlyLoop = b.loopTruncated && !b.callTruncated && !b.evalCallTruncated && !b.forkTruncated;
5857
+ const { segs } = budgetAttributionSegs(b);
5858
+ lines.push("");
5859
+ lines.push("budget");
5860
+ lines.push(` truncated ${segs.join(" \xB7 ")}`);
5861
+ lines.push(
5862
+ r.budget.cycleTruncated ? ` \u2192 some results widened to unknown (not a contract failure) \u2014 includes call cycles (same abstract args; a budget cannot lift them)` : onlyLoop ? ` \u2192 loop state widened (iteration budget, not a contract failure) \u2014 @nudo:budget does not apply; make the bound data-dependent or pin with @nudo:case / @nudo:contract` : ` \u2192 some results widened to unknown (not a contract failure) \u2014 raise @nudo:budget / nudo.analysis.maxForks or simplify recursion`
5863
+ );
5864
+ }
5865
+ if (r.signatures.length > 0) {
5866
+ lines.push("");
5867
+ lines.push("signatures");
5868
+ for (const s of r.signatures) {
5869
+ const paramStr = s.paramTypes && s.paramTypes.length > 0 ? s.params.map((p, i) => `${p}: ${s.paramTypes[i] ?? "any"}`).join(", ") : s.params.join(", ");
5870
+ const throwsStr = s.throws ? ` throws ${s.throws}` : "";
5871
+ const displayBase = s.display.replace(/\s+throws\s+[\s\S]+$/, "").trim();
5872
+ const shapeK = s.abs?.shape?.k;
5873
+ const looksLikeFakeAny = shapeK === "any" && displayBase.length > 0 && !/^any(\b|$)/.test(displayBase);
5874
+ const retStr = !looksLikeFakeAny && shapeK ? formatShape(s.abs) || displayBase.replace(/\s+#\w+$/, "").trim() : displayBase.replace(/\s+#\w+$/, "").trim();
5875
+ lines.push(` ${s.name}(${paramStr}) => ${retStr}${throwsStr}`);
5876
+ if (opts.verbose) {
5877
+ for (const ln of s.detail.split("\n").slice(1)) {
5878
+ lines.push(` ${ln}`);
5879
+ }
5880
+ }
5881
+ }
5882
+ }
5883
+ if (r.issues.length === 0) {
5884
+ lines.push("");
5885
+ lines.push("(no issues)");
5886
+ } else {
5887
+ lines.push("");
5888
+ lines.push("issues");
5889
+ for (const i of r.issues) {
5890
+ const loc = i.line != null ? `L${i.line}` : "";
5891
+ const head = [i.severity.toUpperCase(), loc, i.fn].filter(Boolean).join(" ");
5892
+ lines.push(` [${head}] ${i.message} (${i.code})`);
5893
+ const showDetail = i.severity === "error" || opts.verbose;
5894
+ if (showDetail && i.actual) lines.push(` actual: ${i.actual}`);
5895
+ if (showDetail && i.expected) lines.push(` expected: ${i.expected}`);
5896
+ if (i.suggestion) {
5897
+ lines.push(` \u2192 ${i.suggestion}`);
5898
+ } else if (CONTRACT_FIX_CODES.has(i.code)) {
5899
+ lines.push(` \u2192 add a refine / sidecar contract, or fix the call-site value`);
5900
+ }
5901
+ if (CONTRACT_FIX_CODES.has(i.code)) {
5902
+ lines.push(` fix: nudo contract --draft (emit a sidecar draft you can edit)`);
5903
+ }
5904
+ }
5905
+ }
5906
+ return lines.join("\n");
5907
+ }
5908
+ function escapeGhaData(s) {
5909
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
5910
+ }
5911
+ function escapeGhaProp(s) {
5912
+ return escapeGhaData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
5913
+ }
5914
+ function formatGithubAnnotations(r, opts = {}) {
5915
+ const lines = [];
5916
+ let file = r.file;
5917
+ if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
5918
+ file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
5919
+ }
5920
+ for (const i of r.issues) {
5921
+ const level = i.severity === "error" ? "error" : i.severity === "warning" ? "warning" : "notice";
5922
+ const line = i.line ?? 1;
5923
+ const col = (i.column ?? 0) + 1;
5924
+ const title = escapeGhaProp(i.code);
5925
+ const msgBits = [i.message];
5926
+ if (i.actual && i.expected) msgBits.push(`${i.actual} \u22AD ${i.expected}`);
5927
+ else if (i.suggestion) msgBits.push(i.suggestion);
5928
+ if (CONTRACT_FIX_CODES.has(i.code)) {
5929
+ msgBits.push(`fix: nudo contract --draft`);
5930
+ }
5931
+ const msg = escapeGhaData(msgBits.join(" \xB7 "));
5932
+ const props = [
5933
+ `file=${escapeGhaProp(file)}`,
5934
+ `line=${line}`,
5935
+ `col=${col}`,
5936
+ `title=${title}`
5937
+ ].join(",");
5938
+ lines.push(`::${level} ${props}::${msg}`);
5939
+ }
5940
+ return lines.join("\n");
5941
+ }
5942
+ function formatGitlabCodeQuality(r, opts = {}) {
5943
+ let file = r.file;
5944
+ if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
5945
+ file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
5946
+ }
5947
+ return r.issues.map((i) => {
5948
+ const begin = i.line ?? 1;
5949
+ const severity = i.severity === "error" ? "major" : i.severity === "warning" ? "minor" : "info";
5950
+ const description = i.actual && i.expected ? `${i.message} (${i.actual} \u22AD ${i.expected})` : i.message;
5951
+ const fingerprint = `${file}:${i.code}:${begin}:${i.message}`;
5952
+ return {
5953
+ description,
5954
+ check_name: i.code,
5955
+ fingerprint,
5956
+ severity,
5957
+ location: { path: file, lines: { begin } }
5958
+ };
5959
+ });
5960
+ }
5961
+
5536
5962
  export {
5537
5963
  listFnDirectiveScopes,
5538
5964
  findFnDirectiveScope,
@@ -5568,6 +5994,7 @@ export {
5568
5994
  execNudoModule,
5569
5995
  resetNudoModuleExecCache,
5570
5996
  extractAssumeSegs,
5997
+ extractInvariantSegs,
5571
5998
  fnTopLevelLocalNames,
5572
5999
  extractRefinesFromSource,
5573
6000
  refineToIndexedFull,
@@ -5610,5 +6037,12 @@ export {
5610
6037
  stableAnalyzeKeySource,
5611
6038
  listTopFunctions,
5612
6039
  scanLiteralCalls,
5613
- checkInjectedDomainEvidence
6040
+ checkInjectedDomainEvidence,
6041
+ actionsForIssue,
6042
+ serializeCheckJson,
6043
+ serializeCheckJsonMulti,
6044
+ budgetAttributionSegs,
6045
+ formatCheckReport,
6046
+ formatGithubAnnotations,
6047
+ formatGitlabCodeQuality
5614
6048
  };