@nudojs/core 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,12 +2,14 @@ import "./chunk-US532PZA.js";
2
2
  import {
3
3
  INTERFACE_SOURCE_ABBR,
4
4
  NudoSidecarError,
5
+ actionsForIssue,
5
6
  buildArgsFromAssume,
6
7
  canSkipLiteralCallScan,
7
8
  collectAbsFreeVars,
8
9
  effectiveInterface,
9
10
  evictGeneralizeMemoForPaths,
10
11
  execNudoModule,
12
+ extractAllLoadSpecs,
11
13
  extractAssumeSegs,
12
14
  extractBalancedParens,
13
15
  extractDeclaredThrows,
@@ -23,8 +25,11 @@ import {
23
25
  findFnDirectiveScope,
24
26
  fnDirectiveCommentLines,
25
27
  fnTopLevelLocalNames,
28
+ formatCheckReport,
26
29
  formatConstraint,
27
30
  formatEffectiveInterfaceDisplay,
31
+ formatGithubAnnotations,
32
+ formatGitlabCodeQuality,
28
33
  formatInterfaceTierLine,
29
34
  generalizeAll,
30
35
  generalizeFromAst,
@@ -57,6 +62,8 @@ import {
57
62
  scanContractSegments,
58
63
  scanLiteralCalls,
59
64
  scanThrowsDecl,
65
+ serializeCheckJson,
66
+ serializeCheckJsonMulti,
60
67
  setInterfaceDiagCollector,
61
68
  setRefineDiagCollector,
62
69
  sidecarClosureFingerprint,
@@ -67,7 +74,7 @@ import {
67
74
  takeInterfaceDiagsSince,
68
75
  takeRefineDiags,
69
76
  takeRefineDiagsSince
70
- } from "./chunk-N7AGFMSD.js";
77
+ } from "./chunk-47UPDZHE.js";
71
78
  import {
72
79
  $add,
73
80
  $arguments,
@@ -183,6 +190,7 @@ import {
183
190
  CONSTRAINT_BUILDER_NAMES,
184
191
  CONSTRAINT_EXPR_RE,
185
192
  DEFAULT_MAX_LOOP_ITERS,
193
+ DIAG,
186
194
  FORK_TRUNCATION_LABEL,
187
195
  HOST_EFFECT_LABEL_PREFIX,
188
196
  LOOP_TRUNCATION_LABEL,
@@ -528,7 +536,7 @@ import {
528
536
  withExecPhi,
529
537
  withInvariantSpecs,
530
538
  withVar
531
- } from "./chunk-W3Z77Y2I.js";
539
+ } from "./chunk-NUTYUT3I.js";
532
540
 
533
541
  // src/environment.ts
534
542
  function createEnvironment(parent, bindings = /* @__PURE__ */ new Map()) {
@@ -650,7 +658,7 @@ function checkArg(arg, expect, phi = pTrue) {
650
658
  if (arg.shape.k === "unknown" && !arg.term) {
651
659
  return {
652
660
  severity: "warning",
653
- code: "nudo:arg-opaque",
661
+ code: DIAG.argOpaque,
654
662
  message: `argument type is unknown; cannot verify constraint ${predToString(expect)}`,
655
663
  suggestion: "add a call site or @nudo:case, or provide preconditions via --assume"
656
664
  };
@@ -664,7 +672,7 @@ function checkArg(arg, expect, phi = pTrue) {
664
672
  if (ok === false) {
665
673
  return {
666
674
  severity: "error",
667
- code: "nudo:constraint-violated",
675
+ code: DIAG.constraintViolated,
668
676
  message: `argument ${JSON.stringify(lvR.value)} does not satisfy ${predToString(expect)}`,
669
677
  suggestion: `use a value satisfying the constraint, or relax the function precondition`
670
678
  };
@@ -672,7 +680,7 @@ function checkArg(arg, expect, phi = pTrue) {
672
680
  }
673
681
  return {
674
682
  severity: "warning",
675
- code: "nudo:constraint-unproven",
683
+ code: DIAG.constraintUnproven,
676
684
  message: `cannot prove the argument satisfies ${predToString(expect)}`,
677
685
  suggestion: `not enough information; use --assume or add a case`
678
686
  };
@@ -701,7 +709,7 @@ function checkCall(source, fnName, args, phi = pTrue) {
701
709
  if (!g) {
702
710
  diags.push({
703
711
  severity: "error",
704
- code: "nudo:fn-not-found",
712
+ code: DIAG.fnNotFound,
705
713
  message: `function ${fnName} not found`
706
714
  });
707
715
  return diags;
@@ -709,7 +717,7 @@ function checkCall(source, fnName, args, phi = pTrue) {
709
717
  if (args.length !== g.params.length) {
710
718
  diags.push({
711
719
  severity: "error",
712
- code: "nudo:arg-count",
720
+ code: DIAG.argCount,
713
721
  message: `${fnName} expects ${g.params.length} argument(s), got ${args.length}`,
714
722
  fn: fnName
715
723
  });
@@ -724,7 +732,7 @@ function checkCall(source, fnName, args, phi = pTrue) {
724
732
  if (g.symbolic.conf === "partial" || g.symbolic.conf === "opaque") {
725
733
  diags.push({
726
734
  severity: "info",
727
- code: "nudo:partial-result",
735
+ code: DIAG.partialResult,
728
736
  message: `${fnName}(...) result confidence ${g.symbolic.conf}`,
729
737
  suggestion: "add a call site or constraint to improve precision",
730
738
  fn: fnName
@@ -761,6 +769,9 @@ function formatDiagnostics(diags) {
761
769
  }).join("\n");
762
770
  }
763
771
 
772
+ // src/algebra/check.ts
773
+ import { isBuiltin } from "module";
774
+
764
775
  // src/algebra/postcondition.ts
765
776
  function proved() {
766
777
  return { status: "proved" };
@@ -1489,7 +1500,7 @@ function interfaceDriftIssues(candidates, callRecords, evalResult) {
1489
1500
  if (leqAbs(today2, expected2).ok && leqAbs(expected2, today2).ok) continue;
1490
1501
  out.push({
1491
1502
  severity: "warning",
1492
- code: "nudo:interface-drift",
1503
+ code: DIAG.interfaceDrift,
1493
1504
  message: `${cand.fnName}[${param}]: persisted @generated segment \u2260 today's call-site domain`,
1494
1505
  actual: formatAbs(today2),
1495
1506
  expected: formatConstraint(constraint),
@@ -1514,7 +1525,7 @@ function interfaceDriftIssues(candidates, callRecords, evalResult) {
1514
1525
  if (leqAbs(today, expected).ok && leqAbs(expected, today).ok) continue;
1515
1526
  out.push({
1516
1527
  severity: "warning",
1517
- code: "nudo:interface-drift",
1528
+ code: DIAG.interfaceDrift,
1518
1529
  message: `${cand.fnName}[return]: persisted @generated segment \u2260 today's inferred return`,
1519
1530
  actual: formatAbs(today),
1520
1531
  expected: formatConstraint(retC),
@@ -1715,7 +1726,9 @@ function moduleMapId(m) {
1715
1726
  function checkMemoKey(filePath, source, identityOpts, deps, sidecarFp) {
1716
1727
  return [
1717
1728
  hashSource(source),
1718
- filePath,
1729
+ // filePath 过 stablePathKey 归一(与依赖索引/逐出同口径):同一文件的
1730
+ // 裸路径与 file:// URL 两种形态不得各占一条 memo 条目(命中率损耗)。
1731
+ stablePathKey(filePath),
1719
1732
  `${loadModuleId(identityOpts.loadModule)}:${identityOpts.fromFile ?? ""}`,
1720
1733
  deps.fp,
1721
1734
  sidecarFp ?? "-",
@@ -1933,7 +1946,7 @@ function structuralAssignIssues(records) {
1933
1946
  if (!leq.ok) {
1934
1947
  out.push({
1935
1948
  severity: "error",
1936
- code: "nudo:assign-mismatch",
1949
+ code: DIAG.assignMismatch,
1937
1950
  message: `${r.name}: assignment \u22AD existing shape`,
1938
1951
  actual: formatAbs(r.next),
1939
1952
  expected: formatAbs(r.prev),
@@ -2076,7 +2089,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
2076
2089
  const field = fieldPath.join(".");
2077
2090
  out.push({
2078
2091
  severity: "error",
2079
- code: "nudo:case-inconsistency",
2092
+ code: DIAG.caseInconsistency,
2080
2093
  message: `${fnName} case "${caseName}": witness \u22AD contract`,
2081
2094
  actual: formatAbs(arg),
2082
2095
  expected: `missing field ${field}`,
@@ -2100,7 +2113,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
2100
2113
  const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2101
2114
  out.push({
2102
2115
  severity: "error",
2103
- code: "nudo:case-inconsistency",
2116
+ code: DIAG.caseInconsistency,
2104
2117
  message: `${fnName} case "${caseName}": witness \u22AD contract`,
2105
2118
  actual: formatAbs(arg),
2106
2119
  expected: formatConstraint(entry.constraint),
@@ -2116,7 +2129,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
2116
2129
  const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2117
2130
  out.push({
2118
2131
  severity: "error",
2119
- code: "nudo:case-inconsistency",
2132
+ code: DIAG.caseInconsistency,
2120
2133
  message: `${fnName} case "${caseName}": witness \u22AD contract`,
2121
2134
  actual: formatAbs(arg),
2122
2135
  expected: formatConstraint(entry.constraint),
@@ -2138,7 +2151,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
2138
2151
  const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2139
2152
  out.push({
2140
2153
  severity: "error",
2141
- code: "nudo:case-inconsistency",
2154
+ code: DIAG.caseInconsistency,
2142
2155
  message: `${fnName} case "${caseName}": witness \u22AD contract`,
2143
2156
  actual: formatAbs(arg),
2144
2157
  expected: predToString(p),
@@ -2365,7 +2378,7 @@ function sidecarDiagIssues(diags) {
2365
2378
  const k = `${d.code}\0${d.message}`;
2366
2379
  if (seen.has(k)) continue;
2367
2380
  seen.add(k);
2368
- const severity = d.code === "nudo:contract-syntax" || d.code === "nudo:directive-syntax" ? "warning" : "error";
2381
+ const severity = d.code === DIAG.contractSyntax || d.code === DIAG.directiveSyntax ? "warning" : "error";
2369
2382
  out.push({ severity, code: d.code, message: d.message });
2370
2383
  }
2371
2384
  return out;
@@ -2387,7 +2400,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
2387
2400
  if (seg.kind === "bad") {
2388
2401
  issues.push({
2389
2402
  severity: "error",
2390
- code: "nudo:assume-invalid",
2403
+ code: DIAG.assumeInvalid,
2391
2404
  message: `${name}: malformed @nudo:assume '${seg.raw}' (expected <param> <= <param> or <param> >= <param>; constant bounds belong to sidecar contracts)`,
2392
2405
  actual: seg.raw,
2393
2406
  expected: "<param> <= <param>",
@@ -2400,7 +2413,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
2400
2413
  if (bad.length > 0) {
2401
2414
  issues.push({
2402
2415
  severity: "error",
2403
- code: "nudo:assume-invalid",
2416
+ code: DIAG.assumeInvalid,
2404
2417
  message: `${name}: @nudo:assume name(s) not bound as parameters or top-level locals (${bad.join(", ")})`,
2405
2418
  actual: bad.join(", "),
2406
2419
  expected: surface || "(no params/locals)",
@@ -2431,7 +2444,7 @@ function invariantSpecsOf(name, source, file, issues) {
2431
2444
  if (seg.kind === "bad") {
2432
2445
  issues.push({
2433
2446
  severity: "warning",
2434
- code: "nudo:invariant-invalid",
2447
+ code: DIAG.invariantInvalid,
2435
2448
  message: `${name}: malformed @nudo:invariant '${seg.raw}' (expected <name><path> <rel> <number>; path = [*] or .member steps)`,
2436
2449
  actual: seg.raw,
2437
2450
  expected: "<name><path> <rel> <number>",
@@ -2443,7 +2456,7 @@ function invariantSpecsOf(name, source, file, issues) {
2443
2456
  if (!bindable.has(seg.name)) {
2444
2457
  issues.push({
2445
2458
  severity: "warning",
2446
- code: "nudo:invariant-invalid",
2459
+ code: DIAG.invariantInvalid,
2447
2460
  message: `${name}: @nudo:invariant name not bound as a parameter or top-level local (${seg.name})`,
2448
2461
  actual: seg.name,
2449
2462
  expected: surface || "(no params/locals)",
@@ -2458,12 +2471,20 @@ function invariantSpecsOf(name, source, file, issues) {
2458
2471
  if (specs.length === 0) return void 0;
2459
2472
  return { specs, display };
2460
2473
  }
2474
+ var OPAQUE_EVIDENCE_HINT = "add @nudo:case or a call site";
2475
+ var OPAQUE_ENV_HINT = "declare the runtime env to type native modules: '/// @nudo:env node' (or web/es) \u2014 see guides/env-harvest.md";
2476
+ function isBuiltinModuleSpec(spec) {
2477
+ if (spec.startsWith(".") || spec.startsWith("/")) return false;
2478
+ const base = (spec.startsWith("node:") ? spec.slice("node:".length) : spec).split("/")[0];
2479
+ return base.length > 0 && isBuiltin(base);
2480
+ }
2461
2481
  function checkSourceInner(filePath, source, file, phi, opts, issues, signatures, names, truncated, identityOpts = opts, depsFp, sidecarFp) {
2462
2482
  const hasRefineDirective = stripStringsKeepComments(source).includes("@nudo:contract");
2463
2483
  const entryNames = localNamedExports(source);
2464
2484
  const exportedNames = sidecarFp !== void 0 ? entryNames : void 0;
2465
2485
  const entryThrowsMode = opts.entryThrows ?? "error";
2466
2486
  const ignoreThrows = opts.ignoreThrows;
2487
+ const opaqueEnvHint = Object.keys(opts.inject?.envGlobals ?? {}).length === 0 && extractFileEnvNames(source).length === 0 && extractAllLoadSpecs(source).some(isBuiltinModuleSpec);
2467
2488
  const driftCandidates = [];
2468
2489
  const returnContracts = /* @__PURE__ */ new Map();
2469
2490
  const returnViolated = /* @__PURE__ */ new Set();
@@ -2574,7 +2595,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2574
2595
  const loc = findFnLoc(file, name);
2575
2596
  issues.push({
2576
2597
  severity: entryThrowsMode === "warning" ? "warning" : "error",
2577
- code: "nudo:entry-may-throw",
2598
+ code: DIAG.entryMayThrow,
2578
2599
  message: `${name} (export): may throw ${gateDisplay}`,
2579
2600
  actual: `${name}(\u2026) throws ${gateDisplay}`,
2580
2601
  expected: "entry total, or @nudo:throws / try-catch",
@@ -2599,7 +2620,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2599
2620
  }
2600
2621
  issues.push({
2601
2622
  severity: "warning",
2602
- code: "nudo:no-signature",
2623
+ code: DIAG.noSignature,
2603
2624
  message: `${name}: could not generalize a symbolic Abs`,
2604
2625
  suggestion: "add @nudo:case or make the body algebraically evaluable",
2605
2626
  fn: name
@@ -2619,7 +2640,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2619
2640
  const loc = findFnLoc(file, name);
2620
2641
  issues.push({
2621
2642
  severity: entryThrowsMode === "warning" ? "warning" : "error",
2622
- code: "nudo:entry-may-throw",
2643
+ code: DIAG.entryMayThrow,
2623
2644
  message: `${name} (export): may throw ${gateDisplay}`,
2624
2645
  actual: `${formatEntrySigLine(name, g, gateDisplay)}`,
2625
2646
  expected: "entry total, or @nudo:throws / try-catch",
@@ -2663,7 +2684,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2663
2684
  const where = symbolicTrueUnknown ? `${name} => unknown` : `${name} param ${g.params[unknownParamIdx] ?? `#${unknownParamIdx}`}: unknown`;
2664
2685
  issues.push({
2665
2686
  severity: "warning",
2666
- code: "nudo:unknown-inference",
2687
+ code: DIAG.unknownInference,
2667
2688
  message: `${name}: signature has true unknown (inference failed)`,
2668
2689
  actual: where,
2669
2690
  expected: "computable Abs (any = unconstrained, unknown = engine debt)",
@@ -2684,7 +2705,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2684
2705
  const surface = [...contractNames].filter((n) => !n.startsWith("_p") && n !== "_").join(", ");
2685
2706
  issues.push({
2686
2707
  severity: "error",
2687
- code: "nudo:interface-param-mismatch",
2708
+ code: DIAG.interfaceParamMismatch,
2688
2709
  message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
2689
2710
  actual: unknownParams.join(", "),
2690
2711
  expected: surface || g.params.join(", ") || "(no params)",
@@ -2697,7 +2718,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2697
2718
  if (eff.conflict.params.length > 0) {
2698
2719
  issues.push({
2699
2720
  severity: "error",
2700
- code: "nudo:interface-conflict",
2721
+ code: DIAG.interfaceConflict,
2701
2722
  message: `${name}: handwritten contract conjunction unsatisfiable (${eff.conflict.params.join(", ")})`,
2702
2723
  suggestion: "check whether the source @nudo:contract and the same-name sidecar binding have contradictory constant bounds",
2703
2724
  fn: name
@@ -2706,7 +2727,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2706
2727
  if (eff.conflict.returns) {
2707
2728
  issues.push({
2708
2729
  severity: "error",
2709
- code: "nudo:interface-conflict",
2730
+ code: DIAG.interfaceConflict,
2710
2731
  message: `${name}: handwritten contract conjunction unsatisfiable on the return slot`,
2711
2732
  suggestion: "check whether the source @nudo:contract return and the sidecar fn() return constraint are contradictory (the return slot is not enforced when they are)",
2712
2733
  fn: name
@@ -2742,9 +2763,9 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2742
2763
  if (g.symbolic.conf === "opaque" && !truncated.has(name)) {
2743
2764
  issues.push({
2744
2765
  severity: "info",
2745
- code: "nudo:opaque-result",
2766
+ code: DIAG.opaqueResult,
2746
2767
  message: `${name}(...): conf=opaque (path not covered, or native)`,
2747
- suggestion: "add @nudo:case or a call site",
2768
+ suggestion: opaqueEnvHint ? OPAQUE_ENV_HINT : OPAQUE_EVIDENCE_HINT,
2748
2769
  fn: name
2749
2770
  });
2750
2771
  }
@@ -2753,7 +2774,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2753
2774
  if (label === FORK_TRUNCATION_LABEL) {
2754
2775
  issues.push({
2755
2776
  severity: "info",
2756
- code: "nudo:fork-truncated",
2777
+ code: DIAG.forkTruncated,
2757
2778
  message: `Branch expansion was truncated (fork budget); affected results widened to unknown#opaque (budget)`,
2758
2779
  suggestion: "optional: raise nudo.analysis.maxForks, or @nudo:budget forks=\u2026 on the function \u2014 non-blocking"
2759
2780
  });
@@ -2762,7 +2783,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2762
2783
  if (label === PROMISE_MICRO_OVERFLOW_LABEL) {
2763
2784
  issues.push({
2764
2785
  severity: "info",
2765
- code: "nudo:promise-micro-truncated",
2786
+ code: DIAG.promiseMicroTruncated,
2766
2787
  message: `Promise microtask queue hit its hard cap; further then/catch callbacks were dropped (results stay fail-closed)`,
2767
2788
  suggestion: "optional: reduce top-level Promise.then fan-out \u2014 non-blocking"
2768
2789
  });
@@ -2771,7 +2792,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2771
2792
  if (label === PROMISE_MICRO_ERROR_LABEL) {
2772
2793
  issues.push({
2773
2794
  severity: "info",
2774
- code: "nudo:promise-micro-error",
2795
+ code: DIAG.promiseMicroError,
2775
2796
  message: `A promise microtask threw while draining; the error was recorded and did not rewrite the synchronous return value`,
2776
2797
  suggestion: "optional: inspect the then/catch callback for a throw path \u2014 non-blocking"
2777
2798
  });
@@ -2780,7 +2801,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2780
2801
  if (label === MATH_FOLD_ERROR_LABEL) {
2781
2802
  issues.push({
2782
2803
  severity: "info",
2783
- code: "nudo:math-fold-error",
2804
+ code: DIAG.mathFoldError,
2784
2805
  message: `A Math native fold threw during analysis (host-tampered or divergent Math.*); the result was widened to number`,
2785
2806
  suggestion: "optional: keep Math.* builtins unmodified in analyzed code, or mock them with @nudo:mock \u2014 non-blocking"
2786
2807
  });
@@ -2790,7 +2811,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2790
2811
  const hostName = label.slice(HOST_EFFECT_LABEL_PREFIX.length);
2791
2812
  issues.push({
2792
2813
  severity: "info",
2793
- code: "nudo:host-effect-blocked",
2814
+ code: DIAG.hostEffectBlocked,
2794
2815
  message: `Host function '${hostName}' is not executed during analysis (network/timer side effect); result widened to unknown#opaque`,
2795
2816
  suggestion: "optional: mock it with @nudo:mock / @nudo:env, or pass the value in from a call site",
2796
2817
  fn: hostName
@@ -2800,7 +2821,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2800
2821
  if (label === LOOP_TRUNCATION_LABEL) {
2801
2822
  issues.push({
2802
2823
  severity: "info",
2803
- code: "nudo:loop-truncated",
2824
+ code: DIAG.loopTruncated,
2804
2825
  message: `A loop did not terminate within its iteration budget (abstract unroll cap reached and the exit condition stayed concrete-true); affected loop state was widened (bounded, not exact)`,
2805
2826
  suggestion: "optional: restructure so the exit condition becomes abstract (data-dependent) within a few iterations, or pin the result with @nudo:case / @nudo:contract \u2014 non-blocking"
2806
2827
  });
@@ -2811,7 +2832,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2811
2832
  const calls = reasons.has("calls");
2812
2833
  issues.push({
2813
2834
  severity: "info",
2814
- code: "nudo:recursion-truncated",
2835
+ code: DIAG.recursionTruncated,
2815
2836
  message: allCycle ? `Recursive evaluation of '${label}' hit a call cycle (same abstract arguments already on the stack); result widened to unknown#opaque (cycle \u2014 budget cannot lift this)` : `Recursive evaluation of '${label}' was truncated (${depth && calls ? "depth/size budget" : depth ? "call-depth budget" : calls ? "total-calls budget" : "depth/size budget"}); result widened to unknown#opaque (budget \u2014 not inference debt)`,
2816
2837
  suggestion: allCycle ? "a call cycle is not a budget: make the recursion arguments vary per level, narrow the base case, or declare @nudo:contract \u2014 raising @nudo:budget will not change this" : depth && !calls ? "raise the function budget with @nudo:budget depth=\u2026 (or narrow the recursion base case / declare @nudo:contract return)" : calls && !depth ? "raise the function budget with @nudo:budget calls=\u2026 (or narrow the recursion base case / declare @nudo:contract return)" : "raise the function budget with @nudo:budget calls=\u2026 depth=\u2026 (or narrow the recursion base case / declare @nudo:contract return)",
2817
2838
  reason: allCycle ? "cycle" : depth && calls ? "budget" : depth ? "depth" : calls ? "calls" : "budget",
@@ -2934,7 +2955,7 @@ function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, in
2934
2955
  if (assumeDisplay && assumeDisplay.length > 0) {
2935
2956
  relied.push({
2936
2957
  severity: "info",
2937
- code: "nudo:assume-relied",
2958
+ code: DIAG.assumeRelied,
2938
2959
  message: `${fnName}: return contract proved via @nudo:assume ${assumeDisplay.join("; ")}`,
2939
2960
  actual: `proof relies on user lemma${assumeDisplay.length > 1 ? "s" : ""}`,
2940
2961
  expected: "engine verifies the obligation, not the lemma itself",
@@ -2945,7 +2966,7 @@ function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, in
2945
2966
  if (invariantDisplay && invariantDisplay.length > 0) {
2946
2967
  relied.push({
2947
2968
  severity: "info",
2948
- code: "nudo:invariant-relied",
2969
+ code: DIAG.invariantRelied,
2949
2970
  message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
2950
2971
  actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
2951
2972
  expected: "engine verifies the obligation, not the loop invariant itself",
@@ -2958,7 +2979,7 @@ function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, in
2958
2979
  const push = (severity, actual, expected, suggestion) => [
2959
2980
  {
2960
2981
  severity,
2961
- code: severity === "error" ? "nudo:constraint-violated" : "nudo:unproven-return",
2982
+ code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
2962
2983
  message: `${fnName}: return value \u22AD @nudo:contract return ${cName}`,
2963
2984
  actual,
2964
2985
  expected,
@@ -2981,361 +3002,6 @@ function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, in
2981
3002
  `${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
2982
3003
  );
2983
3004
  }
2984
-
2985
- // src/algebra/check-report.ts
2986
- function actionsForIssue(i) {
2987
- const draft = {
2988
- kind: "draft",
2989
- command: "nudo contract --draft",
2990
- label: "emit a sidecar draft you can edit"
2991
- };
2992
- switch (i.code) {
2993
- case "nudo:constraint-violated":
2994
- return [
2995
- {
2996
- kind: "callsite",
2997
- label: "use a value satisfying the constraint",
2998
- ...i.expected ? { hint: i.expected } : {}
2999
- },
3000
- {
3001
- kind: "relax",
3002
- label: "relax the precondition (edit *.nudo.js / @nudo:contract)",
3003
- ...i.expected ? { hint: i.expected } : {}
3004
- },
3005
- draft
3006
- ];
3007
- case "nudo:assign-mismatch":
3008
- return [
3009
- {
3010
- kind: "callsite",
3011
- label: "align the assigned value with the existing shape",
3012
- ...i.expected ? { hint: i.expected } : {}
3013
- },
3014
- draft
3015
- ];
3016
- case "nudo:entry-may-throw":
3017
- return [
3018
- {
3019
- kind: "draft",
3020
- command: "nudo contract --draft",
3021
- label: "narrow the entry with a sidecar fn({ \u2026 }) param contract \u2014 most honest fix for helpers (also tightens the signature)",
3022
- ...i.fn ? { hint: i.fn } : {}
3023
- },
3024
- {
3025
- kind: "relax",
3026
- label: "declare intentional fail-fast: @nudo:throws Error (or case `!! throws`)",
3027
- ...i.fn ? { hint: i.fn } : {}
3028
- },
3029
- {
3030
- kind: "info",
3031
- label: "or refine / guard / try-catch the entry so it is total"
3032
- },
3033
- {
3034
- kind: "ignore-throws",
3035
- command: "nudo check --ignore-throws TypeError",
3036
- label: "migration switch: ignore this throw class on L2"
3037
- }
3038
- ];
3039
- case "nudo:unproven-return":
3040
- return [
3041
- {
3042
- kind: "relax",
3043
- label: `relax the return contract${i.fn ? ` of ${i.fn}` : ""} to the inferred surface (review the diff \u2014 not a silent green)`,
3044
- ...i.expected ? { hint: i.expected } : {}
3045
- },
3046
- draft,
3047
- {
3048
- kind: "info",
3049
- label: "or narrow the return expression / add a precondition so the bound is implied"
3050
- }
3051
- ];
3052
- case "nudo:unknown-inference":
3053
- case "nudo:opaque-result":
3054
- return [
3055
- { kind: "mock", label: "pin the native/unknown face with @nudo:mock" },
3056
- { kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
3057
- { kind: "info", label: "or add a call site / @nudo:case for evidence" }
3058
- ];
3059
- case "nudo:arg-opaque":
3060
- case "nudo:constraint-unproven":
3061
- return [
3062
- { kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
3063
- { kind: "info", label: "add a call site or @nudo:case" }
3064
- ];
3065
- case "nudo:fork-truncated":
3066
- case "nudo:promise-micro-truncated":
3067
- case "nudo:promise-micro-error":
3068
- return [
3069
- {
3070
- kind: "info",
3071
- label: "results widened to unknown \u2014 raise budget (@nudo:budget forks/calls/depth on the function) or narrow control flow"
3072
- }
3073
- ];
3074
- case "nudo:loop-truncated":
3075
- return [
3076
- {
3077
- kind: "info",
3078
- 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"
3079
- }
3080
- ];
3081
- case "nudo:recursion-truncated":
3082
- return [
3083
- {
3084
- kind: "info",
3085
- 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"
3086
- }
3087
- ];
3088
- case "nudo:host-effect-blocked":
3089
- return [
3090
- { kind: "mock", label: "pin the host API with @nudo:mock / @nudo:env" },
3091
- { kind: "info", label: "results widened to unknown \u2014 or feed the value in from a call site" }
3092
- ];
3093
- case "nudo:interface-drift":
3094
- return [
3095
- {
3096
- kind: "emit",
3097
- command: "nudo contract --emit",
3098
- label: "refresh the @generated segment"
3099
- }
3100
- ];
3101
- case "nudo:dual-entry":
3102
- return [
3103
- {
3104
- kind: "info",
3105
- label: "browser/node records do not cross files \u2014 analyze the entry you ship; mock or skip the other variant"
3106
- }
3107
- ];
3108
- default:
3109
- return i.suggestion ? [{ kind: "info", label: i.suggestion }] : [];
3110
- }
3111
- }
3112
- function serializeCheckJson(r) {
3113
- return {
3114
- version: 1,
3115
- file: r.file,
3116
- ok: r.ok,
3117
- summary: { ...r.summary },
3118
- signatures: r.signatures.map((s) => ({
3119
- name: s.name,
3120
- params: [...s.params],
3121
- ...s.paramTypes ? { paramTypes: [...s.paramTypes] } : {},
3122
- display: s.display,
3123
- detail: s.detail,
3124
- conf: s.conf,
3125
- abs: formatAbs(s.abs),
3126
- ...s.throws ? { throws: s.throws } : {},
3127
- ...s.entry ? { entry: true } : {}
3128
- })),
3129
- issues: r.issues.map((i) => {
3130
- const acts = i.actions ?? actionsForIssue(i);
3131
- return {
3132
- severity: i.severity,
3133
- code: i.code,
3134
- message: i.message,
3135
- ...i.fn !== void 0 ? { fn: i.fn } : {},
3136
- ...i.line !== void 0 ? { line: i.line } : {},
3137
- ...i.column !== void 0 ? { column: i.column } : {},
3138
- ...i.actual !== void 0 ? { actual: i.actual } : {},
3139
- ...i.expected !== void 0 ? { expected: i.expected } : {},
3140
- ...i.suggestion !== void 0 ? { suggestion: i.suggestion } : {},
3141
- ...i.reason !== void 0 ? { reason: i.reason } : {},
3142
- ...acts.length > 0 ? { actions: acts } : {}
3143
- };
3144
- }),
3145
- ...r.budget ? { budget: { ...r.budget } } : {}
3146
- };
3147
- }
3148
- function serializeCheckJsonMulti(reports) {
3149
- const summary = {
3150
- errors: 0,
3151
- warnings: 0,
3152
- infos: 0,
3153
- functions: 0,
3154
- files: reports.length
3155
- };
3156
- let calls = 0;
3157
- let forks = 0;
3158
- let maxCalls = 0;
3159
- let maxForks = 0;
3160
- let callTruncated = false;
3161
- let forkTruncated = false;
3162
- let evalCalls = 0;
3163
- let maxEvalCalls = 0;
3164
- let evalCallTruncated = false;
3165
- let cycleTruncated = false;
3166
- let loopTruncated = false;
3167
- const reasons = /* @__PURE__ */ new Set();
3168
- for (const r of reports) {
3169
- summary.errors += r.summary.errors;
3170
- summary.warnings += r.summary.warnings;
3171
- summary.infos += r.summary.infos;
3172
- summary.functions += r.summary.functions;
3173
- if (r.budget) {
3174
- calls += r.budget.calls;
3175
- forks += r.budget.forks;
3176
- maxCalls = Math.max(maxCalls, r.budget.maxCalls);
3177
- maxForks = Math.max(maxForks, r.budget.maxForks);
3178
- callTruncated = callTruncated || r.budget.callTruncated;
3179
- forkTruncated = forkTruncated || r.budget.forkTruncated;
3180
- evalCalls += r.budget.evalCalls ?? 0;
3181
- maxEvalCalls = Math.max(maxEvalCalls, r.budget.maxEvalCalls ?? 0);
3182
- evalCallTruncated = evalCallTruncated || (r.budget.evalCallTruncated ?? false);
3183
- cycleTruncated = cycleTruncated || (r.budget.cycleTruncated ?? false);
3184
- loopTruncated = loopTruncated || (r.budget.loopTruncated ?? false);
3185
- for (const x of r.budget.reasons ?? []) reasons.add(x);
3186
- }
3187
- }
3188
- const budgetTruncated = callTruncated || evalCallTruncated || forkTruncated || loopTruncated;
3189
- return {
3190
- version: 1,
3191
- kind: "multi",
3192
- ok: reports.every((r) => r.ok),
3193
- summary: {
3194
- ...summary,
3195
- ...budgetTruncated ? { budgetTruncated: true } : {}
3196
- },
3197
- ...budgetTruncated || reports.some((r) => r.budget) ? {
3198
- budget: {
3199
- truncated: budgetTruncated,
3200
- callTruncated,
3201
- forkTruncated,
3202
- calls,
3203
- maxCalls,
3204
- forks,
3205
- maxForks,
3206
- evalCalls,
3207
- maxEvalCalls,
3208
- evalCallTruncated,
3209
- cycleTruncated,
3210
- loopTruncated,
3211
- reasons: [...reasons].sort()
3212
- }
3213
- } : {},
3214
- reports
3215
- };
3216
- }
3217
- var CONTRACT_FIX_CODES = /* @__PURE__ */ new Set([
3218
- "nudo:constraint-violated",
3219
- "nudo:assign-mismatch",
3220
- "nudo:arg-structure",
3221
- "nudo:interface-domain-exceeds",
3222
- "nudo:entry-may-throw",
3223
- "nudo:missing-slot"
3224
- ]);
3225
- function formatCheckReport(r, opts = {}) {
3226
- const lines = [];
3227
- lines.push(`nudo check ${r.file}`);
3228
- lines.push(r.ok ? "OK" : "FAILED");
3229
- lines.push(
3230
- ` ${r.summary.errors} error \xB7 ${r.summary.warnings} warning \xB7 ${r.summary.infos} info \xB7 ${r.summary.functions} fn`
3231
- );
3232
- if (r.budget?.truncated) {
3233
- const onlyLoop = r.budget.loopTruncated && !r.budget.callTruncated && !r.budget.evalCallTruncated && !r.budget.forkTruncated;
3234
- lines.push("");
3235
- lines.push("budget");
3236
- lines.push(
3237
- ` truncated calls ${r.budget.calls}/${r.budget.maxCalls} \xB7 eval calls ${r.budget.evalCalls}/${r.budget.maxEvalCalls} \xB7 forks ${r.budget.forks}/${r.budget.maxForks}${r.budget.callTruncated ? " (calls)" : ""}${r.budget.evalCallTruncated ? " (eval calls)" : ""}${r.budget.forkTruncated ? " (forks)" : ""}${r.budget.loopTruncated ? " (loop iters)" : ""}`
3238
- );
3239
- lines.push(
3240
- 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`
3241
- );
3242
- }
3243
- if (r.signatures.length > 0) {
3244
- lines.push("");
3245
- lines.push("signatures");
3246
- for (const s of r.signatures) {
3247
- const paramStr = s.paramTypes && s.paramTypes.length > 0 ? s.params.map((p, i) => `${p}: ${s.paramTypes[i] ?? "any"}`).join(", ") : s.params.join(", ");
3248
- const throwsStr = s.throws ? ` throws ${s.throws}` : "";
3249
- const displayBase = s.display.replace(/\s+throws\s+[\s\S]+$/, "").trim();
3250
- const shapeK = s.abs?.shape?.k;
3251
- const looksLikeFakeAny = shapeK === "any" && displayBase.length > 0 && !/^any(\b|$)/.test(displayBase);
3252
- const retStr = !looksLikeFakeAny && shapeK ? formatShape(s.abs) || displayBase.replace(/\s+#\w+$/, "").trim() : displayBase.replace(/\s+#\w+$/, "").trim();
3253
- lines.push(` ${s.name}(${paramStr}) => ${retStr}${throwsStr}`);
3254
- if (opts.verbose) {
3255
- for (const ln of s.detail.split("\n").slice(1)) {
3256
- lines.push(` ${ln}`);
3257
- }
3258
- }
3259
- }
3260
- }
3261
- if (r.issues.length === 0) {
3262
- lines.push("");
3263
- lines.push("(no issues)");
3264
- } else {
3265
- lines.push("");
3266
- lines.push("issues");
3267
- for (const i of r.issues) {
3268
- const loc = i.line != null ? `L${i.line}` : "";
3269
- const head = [i.severity.toUpperCase(), loc, i.fn].filter(Boolean).join(" ");
3270
- lines.push(` [${head}] ${i.message} (${i.code})`);
3271
- const showDetail = i.severity === "error" || opts.verbose;
3272
- if (showDetail && i.actual) lines.push(` actual: ${i.actual}`);
3273
- if (showDetail && i.expected) lines.push(` expected: ${i.expected}`);
3274
- if (i.suggestion) {
3275
- lines.push(` \u2192 ${i.suggestion}`);
3276
- } else if (CONTRACT_FIX_CODES.has(i.code)) {
3277
- lines.push(` \u2192 add a refine / sidecar contract, or fix the call-site value`);
3278
- }
3279
- if (CONTRACT_FIX_CODES.has(i.code)) {
3280
- lines.push(` fix: nudo contract --draft (emit a sidecar draft you can edit)`);
3281
- }
3282
- }
3283
- }
3284
- return lines.join("\n");
3285
- }
3286
- function escapeGhaData(s) {
3287
- return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
3288
- }
3289
- function escapeGhaProp(s) {
3290
- return escapeGhaData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
3291
- }
3292
- function formatGithubAnnotations(r, opts = {}) {
3293
- const lines = [];
3294
- let file = r.file;
3295
- if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
3296
- file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
3297
- }
3298
- for (const i of r.issues) {
3299
- const level = i.severity === "error" ? "error" : i.severity === "warning" ? "warning" : "notice";
3300
- const line = i.line ?? 1;
3301
- const col = (i.column ?? 0) + 1;
3302
- const title = escapeGhaProp(i.code);
3303
- const msgBits = [i.message];
3304
- if (i.actual && i.expected) msgBits.push(`${i.actual} \u22AD ${i.expected}`);
3305
- else if (i.suggestion) msgBits.push(i.suggestion);
3306
- if (CONTRACT_FIX_CODES.has(i.code)) {
3307
- msgBits.push(`fix: nudo contract --draft`);
3308
- }
3309
- const msg = escapeGhaData(msgBits.join(" \xB7 "));
3310
- const props = [
3311
- `file=${escapeGhaProp(file)}`,
3312
- `line=${line}`,
3313
- `col=${col}`,
3314
- `title=${title}`
3315
- ].join(",");
3316
- lines.push(`::${level} ${props}::${msg}`);
3317
- }
3318
- return lines.join("\n");
3319
- }
3320
- function formatGitlabCodeQuality(r, opts = {}) {
3321
- let file = r.file;
3322
- if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
3323
- file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
3324
- }
3325
- return r.issues.map((i) => {
3326
- const begin = i.line ?? 1;
3327
- const severity = i.severity === "error" ? "major" : i.severity === "warning" ? "minor" : "info";
3328
- const description = i.actual && i.expected ? `${i.message} (${i.actual} \u22AD ${i.expected})` : i.message;
3329
- const fingerprint = `${file}:${i.code}:${begin}:${i.message}`;
3330
- return {
3331
- description,
3332
- check_name: i.code,
3333
- fingerprint,
3334
- severity,
3335
- location: { path: file, lines: { begin } }
3336
- };
3337
- });
3338
- }
3339
3005
  export {
3340
3006
  $add,
3341
3007
  $arguments,