@nudojs/core 1.15.2 → 1.16.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,10 +2,16 @@ import "./chunk-US532PZA.js";
2
2
  import {
3
3
  INTERFACE_SOURCE_ABBR,
4
4
  NudoSidecarError,
5
+ absWithStableTermIds,
5
6
  actionsForIssue,
6
7
  buildArgsFromAssume,
7
8
  canSkipLiteralCallScan,
9
+ checkArg,
10
+ checkCall,
8
11
  collectAbsFreeVars,
12
+ contractSegmentLocs,
13
+ ctorParamCountOf,
14
+ deferredExecutionSpans,
9
15
  effectiveInterface,
10
16
  evictGeneralizeMemoForPaths,
11
17
  execNudoModule,
@@ -27,6 +33,7 @@ import {
27
33
  fnTopLevelLocalNames,
28
34
  formatCheckReport,
29
35
  formatConstraint,
36
+ formatDiagnostics,
30
37
  formatEffectiveInterfaceDisplay,
31
38
  formatGithubAnnotations,
32
39
  formatGitlabCodeQuality,
@@ -35,20 +42,24 @@ import {
35
42
  generalizeFromAst,
36
43
  generatedExportNames,
37
44
  getGeneralizeMemoSize,
45
+ guardLoadModule,
38
46
  hashSource,
39
47
  interfaceDiagCount,
40
48
  interfaceSourceOf,
41
49
  interfaceTierOf,
42
50
  isNodeModulesPath,
51
+ listClassEntryNames,
43
52
  listFnDirectiveScopes,
44
53
  listFunctionNames,
45
54
  listTopFunctions,
46
55
  loadModuleDepsFingerprint,
47
56
  localNamedExports,
57
+ nodeSpan,
48
58
  normPath,
49
59
  parseEnvPayload,
50
60
  parseMockModulePayload,
51
61
  parseNudoImportPayload,
62
+ reachableTopBindings,
52
63
  refineDiagCount,
53
64
  refineToIndexedFull,
54
65
  resetGeneralizeMemo,
@@ -56,6 +67,7 @@ import {
56
67
  resetNudoModuleExecCache,
57
68
  resetSidecarLoadFailureCache,
58
69
  resolveDepPath,
70
+ resolveExportSlotCall,
59
71
  scanBudgetDecl,
60
72
  scanCaseArgSpans,
61
73
  scanCaseTags,
@@ -66,6 +78,7 @@ import {
66
78
  serializeCheckJsonMulti,
67
79
  setInterfaceDiagCollector,
68
80
  setRefineDiagCollector,
81
+ sidecarAutoBindAllowed,
69
82
  sidecarClosureFingerprint,
70
83
  sidecarPathOf,
71
84
  stableAnalyzeKeySource,
@@ -74,7 +87,7 @@ import {
74
87
  takeInterfaceDiagsSince,
75
88
  takeRefineDiags,
76
89
  takeRefineDiagsSince
77
- } from "./chunk-NXZSRLCT.js";
90
+ } from "./chunk-POGFCYV2.js";
78
91
  import {
79
92
  $add,
80
93
  $arguments,
@@ -193,6 +206,7 @@ import {
193
206
  DIAG,
194
207
  FORK_TRUNCATION_LABEL,
195
208
  HOST_EFFECT_LABEL_PREFIX,
209
+ LAZY_TEMPLATE_DEPTH,
196
210
  LOOP_TRUNCATION_LABEL,
197
211
  MATH_FOLD_ERROR_LABEL,
198
212
  MAX_EVAL_CALL_DEPTH,
@@ -213,6 +227,7 @@ import {
213
227
  absOnly,
214
228
  absShapeKey,
215
229
  absToString,
230
+ accessorReadDomain,
216
231
  add,
217
232
  alphaOf,
218
233
  and,
@@ -227,6 +242,7 @@ import {
227
242
  asAbs,
228
243
  asAbsVal,
229
244
  assignSourceSlots,
245
+ assumeConsumed,
230
246
  assumeFinite,
231
247
  attachContractFaces,
232
248
  attachFnImpl,
@@ -362,6 +378,7 @@ import {
362
378
  isSetAbs,
363
379
  isStrPrim,
364
380
  isSymbolAbs,
381
+ isThrowsIgnored,
365
382
  joinAbs,
366
383
  joinFunctions,
367
384
  joinObjects,
@@ -415,6 +432,7 @@ import {
415
432
  never,
416
433
  not,
417
434
  notAbs,
435
+ noteAssumeConsumed,
418
436
  noteCollectionWrite,
419
437
  noteEvalCallRecord,
420
438
  noteEvalFallback,
@@ -460,6 +478,7 @@ import {
460
478
  relationFn,
461
479
  requiredFnArity,
462
480
  resetAbsCallBudget,
481
+ resetAssumeConsumed,
463
482
  resetEvalCallBudget,
464
483
  resetParseSourceCache,
465
484
  runTranspiled,
@@ -526,6 +545,7 @@ import {
526
545
  tryPromoteHofCallback,
527
546
  tryPromoteReceiverAsArr,
528
547
  tryRunTranspiled,
548
+ tryRunTranspiledDetailed,
529
549
  typeofAbs,
530
550
  undefAbs,
531
551
  union,
@@ -536,7 +556,7 @@ import {
536
556
  withExecPhi,
537
557
  withInvariantSpecs,
538
558
  withVar
539
- } from "./chunk-WKIRPUHZ.js";
559
+ } from "./chunk-OM6SAY2M.js";
540
560
 
541
561
  // src/environment.ts
542
562
  function createEnvironment(parent, bindings = /* @__PURE__ */ new Map()) {
@@ -652,123 +672,6 @@ function describePhi() {
652
672
  return p.op === "true" ? "\u22A4" : "(phi)";
653
673
  }
654
674
 
655
- // src/algebra/diagnostics.ts
656
- function checkArg(arg, expect, phi = pTrue) {
657
- if (!expect || expect.op === "true") return void 0;
658
- if (arg.shape.k === "unknown" && !arg.term) {
659
- return {
660
- severity: "warning",
661
- code: DIAG.argOpaque,
662
- message: `argument type is unknown; cannot verify constraint ${predToString(expect)}`,
663
- suggestion: "add a call site or @nudo:case, or provide preconditions via --assume"
664
- };
665
- }
666
- const combined = arg.pred && arg.pred.op !== "true" ? phi.op === "true" ? arg.pred : { op: "and", args: [phi, arg.pred] } : phi;
667
- if (implies(combined, expect)) return void 0;
668
- const lvR = litValue(arg);
669
- if (lvR.ok && (expect.op === "gt" || expect.op === "ge" || expect.op === "lt" || expect.op === "le")) {
670
- const ok = decideLitCmp(lvR.value, expect);
671
- if (ok === true) return void 0;
672
- if (ok === false) {
673
- return {
674
- severity: "error",
675
- code: DIAG.constraintViolated,
676
- message: `argument ${JSON.stringify(lvR.value)} does not satisfy ${predToString(expect)}`,
677
- suggestion: `use a value satisfying the constraint, or relax the function precondition`
678
- };
679
- }
680
- }
681
- return {
682
- severity: "warning",
683
- code: DIAG.constraintUnproven,
684
- message: `cannot prove the argument satisfies ${predToString(expect)}`,
685
- suggestion: `not enough information; use --assume or add a case`
686
- };
687
- }
688
- function decideLitCmp(lv, p) {
689
- if (typeof lv !== "number" || p.b.op !== "lit" || typeof p.b.value !== "number") {
690
- return void 0;
691
- }
692
- const n = p.b.value;
693
- switch (p.op) {
694
- case "gt":
695
- return lv > n;
696
- case "ge":
697
- return lv >= n;
698
- case "lt":
699
- return lv < n;
700
- case "le":
701
- return lv <= n;
702
- default:
703
- return void 0;
704
- }
705
- }
706
- function checkCall(source, fnName, args, phi = pTrue) {
707
- const diags = [];
708
- const g = generalizeFromAst(fnName, source);
709
- if (!g) {
710
- diags.push({
711
- severity: "error",
712
- code: DIAG.fnNotFound,
713
- message: `function ${fnName} not found`
714
- });
715
- return diags;
716
- }
717
- if (args.length !== g.params.length) {
718
- diags.push({
719
- severity: "error",
720
- code: DIAG.argCount,
721
- message: `${fnName} expects ${g.params.length} argument(s), got ${args.length}`,
722
- fn: fnName
723
- });
724
- return diags;
725
- }
726
- const required = extractParamRequirements(g);
727
- args.forEach((arg, i) => {
728
- const req = required.get(i);
729
- const d = checkArg(arg, req, phi);
730
- if (d) diags.push({ ...d, fn: fnName, argIndex: i });
731
- });
732
- if (g.symbolic.conf === "partial" || g.symbolic.conf === "opaque") {
733
- diags.push({
734
- severity: "info",
735
- code: DIAG.partialResult,
736
- message: `${fnName}(...) result confidence ${g.symbolic.conf}`,
737
- suggestion: "add a call site or constraint to improve precision",
738
- fn: fnName
739
- });
740
- }
741
- return diags;
742
- }
743
- function extractParamRequirements(g) {
744
- const map = /* @__PURE__ */ new Map();
745
- const pred = g.symbolic.pred;
746
- if (!pred || pred.op === "true") return map;
747
- const paramIndex = /* @__PURE__ */ new Map();
748
- g.typeParams.forEach((t, i) => paramIndex.set(t.id, i));
749
- const consider = (p) => {
750
- if (p.op === "and") {
751
- p.args.forEach(consider);
752
- return;
753
- }
754
- if ((p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") && p.a.op === "var" && paramIndex.has(p.a.id) && p.b.op === "lit") {
755
- map.set(paramIndex.get(p.a.id), p);
756
- }
757
- };
758
- consider(pred);
759
- return map;
760
- }
761
- function formatDiagnostics(diags) {
762
- if (diags.length === 0) return "No issues found.";
763
- return diags.map((d) => {
764
- const loc = d.fn ? `${d.fn}${d.argIndex !== void 0 ? `[${d.argIndex}]` : ""}: ` : "";
765
- let s = `[${d.severity}] ${loc}${d.message} (${d.code})`;
766
- if (d.suggestion) s += `
767
- \u2192 ${d.suggestion}`;
768
- return s;
769
- }).join("\n");
770
- }
771
-
772
675
  // src/algebra/check.ts
773
676
  import { isBuiltin } from "module";
774
677
 
@@ -794,12 +697,43 @@ function isValueSetUnknown(ret) {
794
697
  if (ret.shape.k === "any") return true;
795
698
  return false;
796
699
  }
797
- function isBoundedEvidenceMissing(ret, constraint) {
700
+ function isBoundedEvidenceMissing(ret, constraint, phi) {
798
701
  if (constraint.preds.length === 0 && !constraint.members) return false;
799
702
  if (ret.term?.op === "lit") return false;
800
703
  if (ret.pred && ret.pred.op !== "true") return false;
704
+ if (phiSuppliesConstraint(phi, constraint, ret)) return false;
801
705
  return true;
802
706
  }
707
+ function phiSuppliesConstraint(phi, constraint, ret) {
708
+ if (!phi || phi.op === "true") return false;
709
+ const item = retAnchorTerm(ret);
710
+ if (!hasSameItemBoundEvidence(phi, item)) return false;
711
+ return implies(phi, instantiateOnTerm(constraint, item));
712
+ }
713
+ function hasSameItemBoundEvidence(p, item) {
714
+ if (!p) return false;
715
+ switch (p.op) {
716
+ case "gt":
717
+ case "ge":
718
+ case "lt":
719
+ case "le":
720
+ return sameItemTerm(item, p.a);
721
+ case "and":
722
+ case "or":
723
+ return p.args.some((a) => hasSameItemBoundEvidence(a, item));
724
+ case "not":
725
+ return hasSameItemBoundEvidence(p.arg, item);
726
+ default:
727
+ return false;
728
+ }
729
+ }
730
+ function sameItemTerm(item, left) {
731
+ if (termEquals(item, left)) return true;
732
+ if (left.op === "app" && left.fn === "length" && left.args.length === 1) {
733
+ return termEquals(item, left.args[0]);
734
+ }
735
+ return false;
736
+ }
803
737
  function flattenSumArms(ret) {
804
738
  if (ret.shape.k !== "sum") return [ret];
805
739
  const out = [];
@@ -956,7 +890,11 @@ function assertImpliesSingle(arm, constraint, opts, phi) {
956
890
  }
957
891
  return r.status === "unprovable" ? unprovable(`array element: ${r.reason}`) : proved();
958
892
  }
959
- if (constraint.fn) return proved();
893
+ if (constraint.fn) {
894
+ return unprovable(
895
+ "fn-shaped return contract cannot be enforced (declare the returned value as the fn's returns constraint)"
896
+ );
897
+ }
960
898
  return assertScalar(arm, constraint, phi);
961
899
  }
962
900
  function isAnyConstraint(c) {
@@ -981,7 +919,7 @@ function assertShapeFields(ret, constraint, opts) {
981
919
  }
982
920
  continue;
983
921
  }
984
- const r = assertImplies(slot.value, field.constraint, opts);
922
+ const r = assertImplies(accessorReadDomain(ret, key) ?? slot.value, field.constraint, opts);
985
923
  if (r.status === "disproved") {
986
924
  return disproved(`field ${key}: ${r.reason}`);
987
925
  }
@@ -1022,10 +960,14 @@ function assertScalar(ret, constraint, phi) {
1022
960
  }
1023
961
  if (constraintPred.op !== "true") {
1024
962
  if (implies(retPred, constraintPred)) return proved();
963
+ if (phi.op !== "true" && ret.pred && ret.pred.op !== "true" && implies(ret.pred, constraintPred)) {
964
+ return proved();
965
+ }
1025
966
  if (ret.pred && ret.pred.op !== "true" && numericBoundsImplyLocal(ret.pred, constraintPred)) {
1026
967
  return proved();
1027
968
  }
1028
- if (isBoundedEvidenceMissing(ret, constraint)) {
969
+ if (phiSuppliesConstraint(phi, constraint, ret)) return proved();
970
+ if (isBoundedEvidenceMissing(ret, constraint, phi)) {
1029
971
  return unprovable(
1030
972
  `cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
1031
973
  );
@@ -1044,7 +986,7 @@ function assertScalar(ret, constraint, phi) {
1044
986
  }
1045
987
  }
1046
988
  if (constraintPred.op !== "true" || constraint.preds.length > 0) {
1047
- if (isBoundedEvidenceMissing(ret, constraint)) {
989
+ if (isBoundedEvidenceMissing(ret, constraint, phi)) {
1048
990
  return unprovable(
1049
991
  `cannot prove return satisfies ${formatConstraint(constraint)} (no bound evidence on the return value)`
1050
992
  );
@@ -1127,6 +1069,104 @@ function isLengthSelfTerm(t) {
1127
1069
  return t.op === "app" && t.fn === "length" && t.args.length === 1 && t.args[0].op === "var" && t.args[0].id === SELF;
1128
1070
  }
1129
1071
 
1072
+ // src/algebra/check-return.ts
1073
+ function checkReturnConstraint(fnName, cName, constraint, ret, audit = {}) {
1074
+ const {
1075
+ loc,
1076
+ source = "directive",
1077
+ assumeDisplay,
1078
+ invariantDisplay,
1079
+ consumedAssume,
1080
+ phi,
1081
+ phiPremises
1082
+ } = audit;
1083
+ const proof = assertImplies(ret, constraint, phi ? { phi } : void 0);
1084
+ const phiUsedIdx = /* @__PURE__ */ new Set();
1085
+ if (phi && proof.status === "proved" && phiPremises && phiPremises.length > 0) {
1086
+ const withoutPhi = assertImplies(ret, constraint);
1087
+ if (withoutPhi.status !== "proved") {
1088
+ const solo = phiPremises.filter(
1089
+ (p) => phiSuppliesConstraint(p, constraint, ret)
1090
+ );
1091
+ const used = solo.length > 0 ? solo : [...phiPremises];
1092
+ for (const p of used) noteAssumeConsumed(p);
1093
+ phiPremises.forEach((p, i) => {
1094
+ if (used.includes(p)) phiUsedIdx.add(i);
1095
+ });
1096
+ }
1097
+ }
1098
+ const where = () => {
1099
+ if (loc?.line === void 0) return {};
1100
+ return loc.column === void 0 ? { line: loc.line } : { line: loc.line, column: loc.column };
1101
+ };
1102
+ const origin = source === "sidecar" ? `sidecar fn() return slot (${cName})` : `@nudo:contract return ${cName}`;
1103
+ if (proof.status === "proved") {
1104
+ const ledgerUsedIdx = /* @__PURE__ */ new Set();
1105
+ if (assumeDisplay && phiPremises) {
1106
+ const consumed = assumeConsumed();
1107
+ for (let i = 0; i < assumeDisplay.length; i++) {
1108
+ const premise = phiPremises[i];
1109
+ if (premise !== void 0 && consumed.includes(premise)) ledgerUsedIdx.add(i);
1110
+ }
1111
+ }
1112
+ const relied = [];
1113
+ const usedAssume = (assumeDisplay ?? []).filter(
1114
+ (d, i) => consumedAssume?.has(d) === true || ledgerUsedIdx.has(i) || phiUsedIdx.has(i)
1115
+ );
1116
+ if (usedAssume.length > 0) {
1117
+ relied.push({
1118
+ severity: "info",
1119
+ code: DIAG.assumeRelied,
1120
+ message: `${fnName}: return contract proved via @nudo:assume ${usedAssume.join("; ")}`,
1121
+ actual: `proof relies on user lemma${usedAssume.length > 1 ? "s" : ""}`,
1122
+ expected: "engine verifies the obligation, not the lemma itself",
1123
+ suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
1124
+ fn: fnName,
1125
+ ...where()
1126
+ });
1127
+ }
1128
+ if (invariantDisplay && invariantDisplay.length > 0) {
1129
+ relied.push({
1130
+ severity: "info",
1131
+ code: DIAG.invariantRelied,
1132
+ message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
1133
+ actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
1134
+ expected: "engine verifies the obligation, not the loop invariant itself",
1135
+ suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
1136
+ fn: fnName,
1137
+ ...where()
1138
+ });
1139
+ }
1140
+ return relied;
1141
+ }
1142
+ const push = (severity, actual, expected, suggestion) => [
1143
+ {
1144
+ severity,
1145
+ code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
1146
+ message: `${fnName}: return value \u22AD ${origin}`,
1147
+ actual,
1148
+ expected,
1149
+ suggestion,
1150
+ fn: fnName,
1151
+ ...where()
1152
+ }
1153
+ ];
1154
+ if (proof.status === "disproved") {
1155
+ return push(
1156
+ "error",
1157
+ formatAbs(ret),
1158
+ proof.reason,
1159
+ proof.reason.includes("nullish") ? `use nullable(${formatConstraint(constraint)}) or union(..., lit(null)) to allow nullish returns` : `return a value satisfying ${formatConstraint(constraint)}`
1160
+ );
1161
+ }
1162
+ return push(
1163
+ "warning",
1164
+ formatAbs(ret),
1165
+ formatConstraint(constraint),
1166
+ `${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
1167
+ );
1168
+ }
1169
+
1130
1170
  // src/algebra/projection.ts
1131
1171
  var PROJECTABLE_CONF = /* @__PURE__ */ new Set(["exact", "path"]);
1132
1172
  var ARR_FALLBACK_CONF = /* @__PURE__ */ new Set([
@@ -1134,6 +1174,9 @@ var ARR_FALLBACK_CONF = /* @__PURE__ */ new Set([
1134
1174
  "path",
1135
1175
  "widened"
1136
1176
  ]);
1177
+ function isHiddenInternalSlot(k, slot) {
1178
+ return slot.internal === true && canonicalArrayIndex(k) === void 0;
1179
+ }
1137
1180
  function absToConstraint(a, budget = new ProjectionBudget()) {
1138
1181
  if (!PROJECTABLE_CONF.has(a.conf)) return void 0;
1139
1182
  const stop = budget.enter(a);
@@ -1162,7 +1205,7 @@ function absToConstraintInner(a, budget) {
1162
1205
  case "any":
1163
1206
  return a.pred?.op === "or" ? projectOrLiterals(a) : void 0;
1164
1207
  case "obj":
1165
- return projectObj(s.slots, s.index, s.open, budget);
1208
+ return projectObj(a, s, budget);
1166
1209
  case "arr":
1167
1210
  return projectArr(s.element, budget);
1168
1211
  case "sum":
@@ -1347,11 +1390,12 @@ function projectOrLiterals(a) {
1347
1390
  }
1348
1391
  return lits.length > 0 ? union(...lits) : void 0;
1349
1392
  }
1350
- function projectObj(slots, index, open, budget) {
1351
- if (index || open) return void 0;
1352
- const fields = {};
1353
- for (const [k, slot] of Object.entries(slots)) {
1354
- const fc = absToConstraint(slot.value, budget);
1393
+ function projectObj(a, s, budget) {
1394
+ if (s.index || s.open) return void 0;
1395
+ const fields = /* @__PURE__ */ Object.create(null);
1396
+ for (const [k, slot] of Object.entries(s.slots)) {
1397
+ if (isHiddenInternalSlot(k, slot)) continue;
1398
+ const fc = absToConstraint(accessorReadDomain(a, k) ?? slot.value, budget);
1355
1399
  if (!fc) return void 0;
1356
1400
  fields[k] = { constraint: fc, ...slot.optional ? { optional: true } : {} };
1357
1401
  }
@@ -1590,11 +1634,21 @@ function isDefaultExportName(source, fnName) {
1590
1634
  );
1591
1635
  return re.test(source);
1592
1636
  }
1637
+ function formatEntryParamShape(t) {
1638
+ if (t.shape.k === "unknown") {
1639
+ const term = t.term;
1640
+ if (term?.op === "lit" && (term.value === null || term.value === void 0)) {
1641
+ return formatShape(t);
1642
+ }
1643
+ return "unknown";
1644
+ }
1645
+ return formatShape(t, { showLits: true });
1646
+ }
1593
1647
  function formatEntrySigLine(name, g, throws) {
1594
1648
  const names = g.formals && g.formals.length > 0 ? formalParamSignatureNames(g.formals) : g.params;
1595
1649
  const ps = names.map((p, i) => {
1596
1650
  const t = g.typeParams[i]?.value;
1597
- const shown = !t ? "any" : t.shape.k === "unknown" ? "unknown" : formatShape(t);
1651
+ const shown = !t ? "any" : formatEntryParamShape(t);
1598
1652
  return `${p}: ${shown}`;
1599
1653
  }).join(", ");
1600
1654
  return `${name}(${ps}) => ${formatShape(g.symbolic)} throws ${throws}`;
@@ -1633,14 +1687,25 @@ function findFnLoc(file, name) {
1633
1687
  }
1634
1688
  return {};
1635
1689
  }
1690
+ function formatSigDetail(absVal, name) {
1691
+ return formatAbsMultiline(absWithStableTermIds(absVal), name);
1692
+ }
1636
1693
  function formatSigCached(absVal, name) {
1637
- const hit = sigFormatCache.get(absVal);
1694
+ let byName = sigFormatCache.get(absVal);
1695
+ if (!byName) {
1696
+ byName = /* @__PURE__ */ new Map();
1697
+ sigFormatCache.set(absVal, byName);
1698
+ }
1699
+ const hit = byName.get(name);
1638
1700
  if (hit) return hit;
1639
1701
  const out = {
1640
- display: formatAbs(absVal),
1641
- detail: formatAbsMultiline(absVal, name)
1702
+ // **人类面口径**(`formatShape`):与 `formatCheckReport` 的签名行**逐字
1703
+ // 同源**(Bug 413:机器面此前用 `formatAbs` ⇒ 把 term / pred 段渲进
1704
+ // 对外载荷,其中混着引擎内部计数器 id `len#N` ⇒ 载荷随参数列表位置漂移)。
1705
+ display: formatShape(absVal),
1706
+ detail: formatSigDetail(absVal, name)
1642
1707
  };
1643
- sigFormatCache.set(absVal, out);
1708
+ byName.set(name, out);
1644
1709
  return out;
1645
1710
  }
1646
1711
 
@@ -1667,12 +1732,6 @@ function loadModuleId(fn2) {
1667
1732
  }
1668
1733
  return id;
1669
1734
  }
1670
- function checkDepsFingerprint(source, opts) {
1671
- if (!opts.loadModule || !opts.fromFile) {
1672
- return { fp: "-", paths: [], contents: [], truncated: false, readError: false };
1673
- }
1674
- return loadModuleDepsFingerprint(source, opts.loadModule, opts.fromFile);
1675
- }
1676
1735
  function unindexCheckKey(key) {
1677
1736
  const deps = checkKeyDeps.get(key);
1678
1737
  if (!deps) return;
@@ -1741,13 +1800,23 @@ function checkMemoKey(filePath, source, identityOpts, deps, sidecarFp) {
1741
1800
  runTranspiledOptionsMemoKey(identityOpts.inject),
1742
1801
  // skips 必须进键:同 source 不同 skip 表(host 解析差异 / 测试注入)
1743
1802
  // 不得回放另一档报告。
1744
- skipsKey(identityOpts.skips)
1803
+ skipsKey(identityOpts.skips),
1804
+ // Bug 366:`from`(--from 的跨文件调用点证据)必须进键——同 source 不同
1805
+ // 证据不得互为陈旧命中:无证据那档会把有证据的 drift 吞掉,有证据那档
1806
+ // 会把 drift 回放进无证据的检查(假阳)。
1807
+ fromKey(identityOpts.from)
1745
1808
  ].join("|");
1746
1809
  }
1747
1810
  function skipsKey(skips) {
1748
1811
  if (!skips || skips.size === 0) return "-";
1749
1812
  return [...skips.entries()].map(([name, abs2]) => `${name}:${abs2 ? formatAbs(abs2) : "-"}`).sort().join(",");
1750
1813
  }
1814
+ function fromKey(from) {
1815
+ if (!from || from.length === 0) return "-";
1816
+ return from.map(
1817
+ (r) => `${r.fnName}:${r.args.map((a) => formatAbs(a)).join(",")}:${formatAbs(r.result)}`
1818
+ ).sort().join(";");
1819
+ }
1751
1820
  function checkMemoGet(key) {
1752
1821
  if (!checkReportMemo.has(key)) return null;
1753
1822
  const v2 = checkReportMemo.get(key);
@@ -1785,8 +1854,17 @@ function collectEntryMayThrows(source, fnName, g, file, phi, opts = {}) {
1785
1854
  return runWithMayThrowSession(() => {
1786
1855
  setMayThrowCollector((e) => effects.push(e));
1787
1856
  try {
1788
- const full = evalThrowsOf(source, fnName, entryArgs, opts, phi);
1789
- if (!full) return effects;
1857
+ const outcome = evalThrowsOutcome(source, fnName, entryArgs, opts, phi, file);
1858
+ if (outcome.kind === "unanalyzable") {
1859
+ if (!unsupportedFallsInside(file, fnName, outcome.reason, outcome.loc)) return effects;
1860
+ effects.push({
1861
+ kind: "Error",
1862
+ cause: `entry not analyzed (${outcome.reason}) \u21D2 may-throw undeterminable`
1863
+ });
1864
+ return effects;
1865
+ }
1866
+ if (outcome.kind !== "analyzed") return effects;
1867
+ const full = outcome.result;
1790
1868
  if (full.throws && full.throws.shape.k !== "never") {
1791
1869
  for (const tName of throwAbsToKinds(full.throws)) {
1792
1870
  if (!effects.some((e) => e.kind === tName && e.cause.startsWith("throw"))) {
@@ -1828,7 +1906,13 @@ function invokeAsThrows(fn2, phi = pTrue) {
1828
1906
  throw e;
1829
1907
  }
1830
1908
  }
1831
- function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
1909
+ function declaredThrowsForEntry(source, fnName) {
1910
+ const own = extractDeclaredThrows(source, fnName);
1911
+ if (own !== void 0) return own;
1912
+ if (fnName.includes(".")) return void 0;
1913
+ return extractDeclaredThrows(source, `${fnName}.constructor`);
1914
+ }
1915
+ function evalThrowsOutcome(source, fnName, args, opts = {}, phi = pTrue, file) {
1832
1916
  const runKey = `${source}|${runTranspiledOptionsMemoKey(opts.inject)}|${runTranspiledOptionsMemoKey(opts.modules ? { modules: opts.modules } : void 0)}`;
1833
1917
  if (evalRunMemo.size >= MAX_CHECK_MEMO) {
1834
1918
  const oldest = evalRunMemo.keys().next().value;
@@ -1836,9 +1920,16 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
1836
1920
  }
1837
1921
  let exports = evalRunMemo.get(runKey);
1838
1922
  if (exports === void 0) {
1839
- const run = tryRunTranspiled(source, evalAnalyzeOpts(opts));
1840
- if (run === void 0) return void 0;
1841
- exports = run;
1923
+ const attempt = tryRunTranspiledDetailed(source, evalAnalyzeOpts(opts));
1924
+ if (attempt.run === void 0) {
1925
+ const reason = attempt.fallback?.reason ?? "transpile failed";
1926
+ if (reason.startsWith("unsupported:")) {
1927
+ const loc = attempt.fallback?.loc;
1928
+ return loc ? { kind: "unanalyzable", reason, loc } : { kind: "unanalyzable", reason };
1929
+ }
1930
+ return { kind: "absent" };
1931
+ }
1932
+ exports = attempt.run;
1842
1933
  evalRunMemo.set(runKey, exports);
1843
1934
  }
1844
1935
  const isAbsVal2 = (v2) => !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
@@ -1850,29 +1941,336 @@ function evalThrowsOf(source, fnName, args, opts = {}, phi = pTrue) {
1850
1941
  }
1851
1942
  };
1852
1943
  try {
1853
- if (Object.hasOwn(exports, fnName)) return call(fnName, args);
1944
+ if (!fnName.includes(".")) {
1945
+ const clsAbs = classAbsOf(exports, file, fnName);
1946
+ if (isAbsVal2(clsAbs) && isClassValueAbs(clsAbs)) {
1947
+ return {
1948
+ kind: "analyzed",
1949
+ result: classEntryThrows(clsAbs, file, fnName, phi)
1950
+ };
1951
+ }
1952
+ }
1953
+ if (Object.hasOwn(exports, fnName)) {
1954
+ const r = call(fnName, args);
1955
+ return r ? { kind: "analyzed", result: r } : { kind: "absent" };
1956
+ }
1854
1957
  if (fnName.includes(".")) {
1855
1958
  const [clsName, methodName] = fnName.split(".", 2);
1856
- const clsAbs = exports[clsName ?? ""];
1959
+ const clsAbs = classAbsOf(exports, file, clsName ?? "");
1857
1960
  if (isAbsVal2(clsAbs)) {
1858
- return invokeAsThrows(() => $staticInvoke(clsAbs, methodName ?? "", args), phi);
1961
+ if (classMethodIsStatic(file, clsName ?? "", methodName ?? "") === false) {
1962
+ return {
1963
+ kind: "analyzed",
1964
+ result: invokeAsThrows(() => {
1965
+ const inst = instanceForMethodBridge(
1966
+ clsAbs,
1967
+ clsName ?? "",
1968
+ ctorAnyArgs(file, clsName ?? "")
1969
+ );
1970
+ return $invoke(inst, methodName ?? "", args);
1971
+ }, phi)
1972
+ };
1973
+ }
1974
+ return {
1975
+ kind: "analyzed",
1976
+ result: invokeAsThrows(() => $staticInvoke(clsAbs, methodName ?? "", args), phi)
1977
+ };
1859
1978
  }
1860
- return void 0;
1979
+ return { kind: "absent" };
1861
1980
  }
1862
1981
  if ("default" in exports) {
1863
1982
  const d = exports["default"];
1864
- if (typeof d === "function") return call("default", args);
1983
+ if (typeof d === "function") {
1984
+ const r = call("default", args);
1985
+ return r ? { kind: "analyzed", result: r } : { kind: "absent" };
1986
+ }
1865
1987
  if (isAbsVal2(d)) {
1866
- if (d.shape.k === "fn") return call("default", args);
1988
+ if (d.shape.k === "fn") {
1989
+ const r = call("default", args);
1990
+ return r ? { kind: "analyzed", result: r } : { kind: "absent" };
1991
+ }
1867
1992
  if (d.shape.k === "obj") {
1868
- return invokeAsThrows(() => $invoke(d, fnName, args), phi);
1993
+ return {
1994
+ kind: "analyzed",
1995
+ result: invokeAsThrows(() => $invoke(d, fnName, args), phi)
1996
+ };
1869
1997
  }
1870
1998
  }
1871
1999
  }
2000
+ return { kind: "absent" };
2001
+ } catch {
2002
+ return { kind: "absent" };
2003
+ }
2004
+ }
2005
+ function classAbsOf(exports, file, clsName) {
2006
+ const slot = file ? resolveExportSlotCall(file, clsName, exports) : void 0;
2007
+ if (slot && slot.kind !== "cjs-member") return exports[slot.key];
2008
+ return exports[clsName];
2009
+ }
2010
+ function isClassValueAbs(v2) {
2011
+ return v2.shape.k === "brand" && v2.shape.ctor === true;
2012
+ }
2013
+ function classEntryThrows(clsAbs, file, clsName, phi) {
2014
+ return invokeAsThrows(
2015
+ () => runWithMayThrowSession(() => $new(clsAbs, ctorAnyArgs(file, clsName))),
2016
+ phi
2017
+ );
2018
+ }
2019
+ function classMethodIsStatic(file, clsName, methodName) {
2020
+ if (!file) return void 0;
2021
+ for (const stmt of file.program.body) {
2022
+ const s = stmt;
2023
+ let decl = stmt;
2024
+ if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
2025
+ decl = s.declaration;
2026
+ }
2027
+ const c = decl;
2028
+ if (!c || c.type !== "ClassDeclaration") continue;
2029
+ if (c.id?.name !== clsName) continue;
2030
+ for (const m of c.body?.body ?? []) {
2031
+ const mem = m;
2032
+ if (mem.kind === "constructor") continue;
2033
+ if (classMemberName(mem) !== methodName) continue;
2034
+ return mem.static === true;
2035
+ }
1872
2036
  return void 0;
2037
+ }
2038
+ return void 0;
2039
+ }
2040
+ function ctorAnyArgs(file, clsName) {
2041
+ const n = file ? ctorParamCountOf(file, clsName) : 0;
2042
+ return Array.from({ length: n }, () => abs({ k: "any" }, void 0, pTrue, "path"));
2043
+ }
2044
+ function instanceForMethodBridge(clsAbs, clsName, ctorArgs) {
2045
+ try {
2046
+ return runWithMayThrowSession(() => $new(clsAbs, ctorArgs));
1873
2047
  } catch {
2048
+ return bareInstanceOf(clsAbs, clsName);
2049
+ }
2050
+ }
2051
+ function bareInstanceOf(clsAbs, clsName) {
2052
+ const name = clsAbs.shape.k === "brand" && clsAbs.shape.name.length > 0 ? clsAbs.shape.name : clsName;
2053
+ return abs(
2054
+ {
2055
+ k: "brand",
2056
+ name,
2057
+ shape: abs({ k: "unknown" }, void 0, void 0, "partial")
2058
+ },
2059
+ void 0,
2060
+ void 0,
2061
+ "path"
2062
+ );
2063
+ }
2064
+ function classMemberName(m) {
2065
+ const key = m.key;
2066
+ if (!key) return void 0;
2067
+ return key.name ?? (typeof key.value === "string" ? key.value : void 0);
2068
+ }
2069
+ function isExportsTarget(node) {
2070
+ const n = node;
2071
+ if (!n) return false;
2072
+ if (n.type === "Identifier") return n.name === "exports";
2073
+ if (n.type !== "MemberExpression") return false;
2074
+ const obj2 = n.object;
2075
+ const prop = n.property;
2076
+ return obj2?.type === "Identifier" && obj2.name === "module" && prop?.name === "exports";
2077
+ }
2078
+ function cjsExportName(left) {
2079
+ const l = left;
2080
+ if (!l || l.type !== "MemberExpression" || l.computed === true) return void 0;
2081
+ if (!isExportsTarget(l.object)) return void 0;
2082
+ const prop = l.property;
2083
+ return prop?.name;
2084
+ }
2085
+ function keyNameOf(node) {
2086
+ const n = node;
2087
+ if (!n) return void 0;
2088
+ if (n.type === "Identifier" && typeof n.name === "string") return n.name;
2089
+ if (n.type === "StringLiteral" && typeof n.value === "string") return n.value;
2090
+ return void 0;
2091
+ }
2092
+ function entryValueNode(node) {
2093
+ const n = node;
2094
+ if (!n) return void 0;
2095
+ return n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassDeclaration" || n.type === "ClassExpression" ? node : void 0;
2096
+ }
2097
+ function classMethodEntryNodes(cls, methodName) {
2098
+ const members = cls.body?.body ?? [];
2099
+ let method;
2100
+ for (const m of members) {
2101
+ if (m.kind === "constructor") continue;
2102
+ if (classMemberName(m) !== methodName) continue;
2103
+ method = m;
2104
+ break;
2105
+ }
2106
+ if (!method) return void 0;
2107
+ const isStatic = method.static === true;
2108
+ const nodes = [method];
2109
+ for (const m of members) {
2110
+ if (m === method) continue;
2111
+ if (m.kind === "constructor") {
2112
+ if (!isStatic) nodes.push(m);
2113
+ continue;
2114
+ }
2115
+ if (m.type === "StaticBlock") {
2116
+ if (isStatic) nodes.push(m);
2117
+ continue;
2118
+ }
2119
+ if (m.type === "ClassProperty" || m.type === "ClassPrivateProperty") {
2120
+ if (m.static === true !== isStatic) continue;
2121
+ nodes.push(m.value ?? m);
2122
+ }
2123
+ }
2124
+ return nodes;
2125
+ }
2126
+ function cjsEntryNodes(file, expression, fnName, depth) {
2127
+ const e = expression;
2128
+ if (!e || e.type !== "AssignmentExpression" || e.operator !== "=") return void 0;
2129
+ const right = e.right;
2130
+ if (!right) return void 0;
2131
+ const fromValue = (node) => {
2132
+ const value = entryValueNode(node);
2133
+ if (value) return [value];
2134
+ const ident = node;
2135
+ if (ident?.type === "Identifier" && ident.name) {
2136
+ return entryNodesOf(file, ident.name, depth + 1);
2137
+ }
1874
2138
  return void 0;
2139
+ };
2140
+ if (cjsExportName(e.left) === fnName) return fromValue(right);
2141
+ if (!isExportsTarget(e.left)) return void 0;
2142
+ if (right.type === "ObjectExpression") {
2143
+ for (const p of right.properties ?? []) {
2144
+ const prop = p;
2145
+ if (keyNameOf(prop.key) !== fnName) continue;
2146
+ if (prop.type === "ObjectMethod") return [prop];
2147
+ return fromValue(prop.value);
2148
+ }
2149
+ return void 0;
2150
+ }
2151
+ if (right.type === "Identifier" && right.name === fnName) {
2152
+ return entryNodesOf(file, fnName, depth + 1);
1875
2153
  }
2154
+ return void 0;
2155
+ }
2156
+ function entryNodesOf(file, fnName, depth = 0) {
2157
+ if (!file || depth > 3) return void 0;
2158
+ for (const stmt of file.program.body) {
2159
+ const s = stmt;
2160
+ if (s.type === "ExpressionStatement") {
2161
+ const hit = cjsEntryNodes(file, s.expression, fnName, depth);
2162
+ if (hit) return hit;
2163
+ continue;
2164
+ }
2165
+ let decl = stmt;
2166
+ if (s.type === "ExportNamedDeclaration" || s.type === "ExportDefaultDeclaration") {
2167
+ decl = s.declaration;
2168
+ }
2169
+ const d = decl;
2170
+ if (!d || typeof d !== "object") continue;
2171
+ if (fnName.includes(".")) {
2172
+ const [clsName, methodName] = fnName.split(".", 2);
2173
+ if (d.type !== "ClassDeclaration" || d.id?.name !== clsName) continue;
2174
+ return classMethodEntryNodes(d, methodName ?? "");
2175
+ }
2176
+ if (d.type === "FunctionDeclaration" && d.id?.name === fnName) return [d];
2177
+ if (fnName === "default" && s.type === "ExportDefaultDeclaration") {
2178
+ const t = d.type;
2179
+ if (t === "FunctionDeclaration" || t === "ArrowFunctionExpression" || t === "FunctionExpression" || t === "ClassDeclaration") {
2180
+ return [d];
2181
+ }
2182
+ }
2183
+ if (d.type === "ClassDeclaration" && d.id?.name === fnName) return [d];
2184
+ if (d.type === "VariableDeclaration") {
2185
+ for (const dec of d.declarations ?? []) {
2186
+ const id = dec.id;
2187
+ if (id?.name === fnName) return [dec];
2188
+ }
2189
+ }
2190
+ }
2191
+ return void 0;
2192
+ }
2193
+ function locInSpan(loc, span) {
2194
+ if (!loc || !span) return false;
2195
+ if (loc.line < span.startLine || loc.line > span.endLine) return false;
2196
+ if (loc.line === span.startLine && loc.column < span.startColumn) return false;
2197
+ if (loc.line === span.endLine && loc.column > span.endColumn) return false;
2198
+ return true;
2199
+ }
2200
+ function isModuleInitLoc(file, loc) {
2201
+ if (!loc) return false;
2202
+ if (!locInSpan(loc, nodeSpan(file.program))) return false;
2203
+ for (const span of deferredExecutionSpans(file)) {
2204
+ if (locInSpan(loc, span)) return false;
2205
+ }
2206
+ return true;
2207
+ }
2208
+ function definiteModuleThrowOf(file) {
2209
+ return file ? scanDefiniteThrowSeq(file.program.body) : void 0;
2210
+ }
2211
+ function scanDefiniteThrowSeq(stmts) {
2212
+ for (const stmt of stmts) {
2213
+ const s = stmt;
2214
+ switch (s.type) {
2215
+ case "ThrowStatement": {
2216
+ const at = s.loc?.start;
2217
+ if (at?.line === void 0) return void 0;
2218
+ const kind = thrownKindOf(s.argument);
2219
+ return {
2220
+ line: at.line,
2221
+ column: at.column ?? 0,
2222
+ ...kind !== void 0 ? { kind } : {}
2223
+ };
2224
+ }
2225
+ // 裸块 = 无条件的语句序列 ⇒ 继续下钻
2226
+ case "BlockStatement": {
2227
+ const hit = scanDefiniteThrowSeq(s.body ?? []);
2228
+ if (hit) return hit;
2229
+ continue;
2230
+ }
2231
+ case "ReturnStatement":
2232
+ return void 0;
2233
+ case "IfStatement":
2234
+ case "SwitchStatement":
2235
+ case "TryStatement":
2236
+ case "ForStatement":
2237
+ case "ForInStatement":
2238
+ case "ForOfStatement":
2239
+ case "WhileStatement":
2240
+ case "DoWhileStatement":
2241
+ case "LabeledStatement":
2242
+ case "WithStatement":
2243
+ return void 0;
2244
+ default:
2245
+ continue;
2246
+ }
2247
+ }
2248
+ return void 0;
2249
+ }
2250
+ function thrownKindOf(arg) {
2251
+ const a = arg;
2252
+ const callee = a?.callee;
2253
+ if (callee?.type !== "Identifier" || typeof callee.name !== "string") return void 0;
2254
+ if (a?.type === "NewExpression") return callee.name;
2255
+ return a?.type === "CallExpression" && callee.name.endsWith("Error") ? callee.name : void 0;
2256
+ }
2257
+ var TS_ONLY_STATEMENT_REASON_PREFIX = "unsupported:statement:TS";
2258
+ function unsupportedFallsInside(file, fnName, reason, loc) {
2259
+ if (!file || !loc) return false;
2260
+ if (!reason.startsWith(TS_ONLY_STATEMENT_REASON_PREFIX) && isModuleInitLoc(file, loc)) {
2261
+ return true;
2262
+ }
2263
+ const entryNodes = entryNodesOf(file, fnName);
2264
+ if (!entryNodes || entryNodes.length === 0) return false;
2265
+ for (const node of entryNodes) {
2266
+ if (locInSpan(loc, nodeSpan(node))) return true;
2267
+ }
2268
+ for (const node of entryNodes) {
2269
+ for (const reach of reachableTopBindings(file, node, { includeClasses: true })) {
2270
+ if (locInSpan(loc, nodeSpan(reach.node))) return true;
2271
+ }
2272
+ }
2273
+ return false;
1876
2274
  }
1877
2275
 
1878
2276
  // src/algebra/check-assign.ts
@@ -1884,6 +2282,8 @@ function widenForAssign(a) {
1884
2282
  const el = els.reduce((x, y) => joinAbs(x, y));
1885
2283
  return abs({ k: "arr", element: el }, void 0, void 0, a.conf);
1886
2284
  }
2285
+ const prim = samePrimOfSum(s.members);
2286
+ if (prim) return abs({ k: "prim", type: prim }, void 0, void 0, a.conf);
1887
2287
  return a;
1888
2288
  }
1889
2289
  if (s.k === "tuple") {
@@ -1935,14 +2335,40 @@ function widenForAssign(a) {
1935
2335
  }
1936
2336
  return a;
1937
2337
  }
2338
+ function samePrimOfSum(members) {
2339
+ let out;
2340
+ for (const m of members) {
2341
+ const t = primNameOf(m);
2342
+ if (!t) return void 0;
2343
+ if (out === void 0) out = t;
2344
+ else if (out !== t) return void 0;
2345
+ }
2346
+ return out;
2347
+ }
2348
+ function primNameOf(a) {
2349
+ if (a.shape.k === "prim") return a.shape.type;
2350
+ const lv = a.term?.op === "lit" ? a.term.value : void 0;
2351
+ if (typeof lv === "string") return "string";
2352
+ if (typeof lv === "number") return "number";
2353
+ if (typeof lv === "boolean") return "boolean";
2354
+ if (typeof lv === "bigint") return "bigint";
2355
+ return void 0;
2356
+ }
2357
+ function isAbsLike(v2) {
2358
+ return !!v2 && typeof v2 === "object" && "shape" in v2 && "conf" in v2;
2359
+ }
2360
+ function shapeOnly(a) {
2361
+ return abs(a.shape, void 0, void 0, a.conf);
2362
+ }
1938
2363
  function structuralAssignIssues(records) {
1939
2364
  const out = [];
1940
2365
  for (const r of records) {
1941
2366
  if (!r.prev) continue;
2367
+ if (!isAbsLike(r.prev) || !isAbsLike(r.next)) continue;
1942
2368
  if (r.conditional) continue;
1943
2369
  if (r.next.shape.k === "unknown" && !r.next.term) continue;
1944
2370
  if (r.prev.shape.k === "unknown" && !r.prev.term) continue;
1945
- const leq = leqAbs(widenForAssign(r.next), widenForAssign(r.prev));
2371
+ const leq = leqAbs(widenForAssign(r.next), shapeOnly(widenForAssign(r.prev)));
1946
2372
  if (!leq.ok) {
1947
2373
  out.push({
1948
2374
  severity: "error",
@@ -1961,6 +2387,339 @@ function structuralAssignIssues(records) {
1961
2387
  }
1962
2388
 
1963
2389
  // src/algebra/check-case-scan.ts
2390
+ function parseNumericLiteral(text) {
2391
+ const s = text.trim();
2392
+ if (s === "") return void 0;
2393
+ let sign = 1;
2394
+ let body = s;
2395
+ if (body.startsWith("-")) {
2396
+ sign = -1;
2397
+ body = body.slice(1).trimStart();
2398
+ } else if (body.startsWith("+")) {
2399
+ body = body.slice(1).trimStart();
2400
+ }
2401
+ if (body === "") return void 0;
2402
+ if (body.endsWith("n")) {
2403
+ const d = body.slice(0, -1);
2404
+ if (d.includes("_")) return void 0;
2405
+ if (/^0[xX][0-9a-fA-F]+$/.test(d)) return BigInt(d) * BigInt(sign);
2406
+ if (/^0[bB][01]+$/.test(d)) return BigInt(d) * BigInt(sign);
2407
+ if (/^0[oO][0-7]+$/.test(d)) return BigInt(d) * BigInt(sign);
2408
+ if (/^[0-9]+$/.test(d)) return BigInt(d) * BigInt(sign);
2409
+ return void 0;
2410
+ }
2411
+ if (body.includes("__")) return void 0;
2412
+ const clean = body.replace(/_/g, "");
2413
+ if (clean === "") return void 0;
2414
+ if (/^0[xX][0-9a-fA-F]+$/.test(clean)) return sign * Number(BigInt(clean));
2415
+ if (/^0[bB][01]+$/.test(clean)) return sign * Number(BigInt(clean));
2416
+ if (/^0[oO][0-7]+$/.test(clean)) return sign * Number(BigInt(clean));
2417
+ if (clean === "Infinity") return sign * Infinity;
2418
+ if (clean === "NaN") return NaN;
2419
+ if (/^0[0-7]+$/.test(clean) && !body.includes("_")) return sign * parseInt(clean.slice(1), 8);
2420
+ if (/^(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/.test(clean)) return sign * Number(clean);
2421
+ return void 0;
2422
+ }
2423
+ function parseStringLiteralValue(text) {
2424
+ const t = text.trim();
2425
+ if (t.length < 2) return void 0;
2426
+ const q = t[0];
2427
+ if (q !== '"' && q !== "'") return void 0;
2428
+ if (t[t.length - 1] !== q) return void 0;
2429
+ const inner = t.slice(1, -1);
2430
+ for (let i = 0; i < inner.length; i++) {
2431
+ const ch = inner[i];
2432
+ if (ch === "\\") {
2433
+ i++;
2434
+ continue;
2435
+ }
2436
+ if (ch === q) return void 0;
2437
+ }
2438
+ let out = "";
2439
+ for (let i = 0; i < inner.length; i++) {
2440
+ const ch = inner[i];
2441
+ if (ch !== "\\") {
2442
+ out += ch;
2443
+ continue;
2444
+ }
2445
+ const n = inner[i + 1];
2446
+ if (n === void 0) return void 0;
2447
+ i++;
2448
+ switch (n) {
2449
+ case "n":
2450
+ out += "\n";
2451
+ break;
2452
+ case "t":
2453
+ out += " ";
2454
+ break;
2455
+ case "r":
2456
+ out += "\r";
2457
+ break;
2458
+ case "b":
2459
+ out += "\b";
2460
+ break;
2461
+ case "f":
2462
+ out += "\f";
2463
+ break;
2464
+ case "v":
2465
+ out += "\v";
2466
+ break;
2467
+ case "0":
2468
+ out += "\0";
2469
+ break;
2470
+ case "x": {
2471
+ const hex = inner.slice(i + 1, i + 3);
2472
+ if (!/^[0-9a-fA-F]{2}$/.test(hex)) return void 0;
2473
+ out += String.fromCharCode(parseInt(hex, 16));
2474
+ i += 2;
2475
+ break;
2476
+ }
2477
+ case "u": {
2478
+ if (inner[i + 1] === "{") {
2479
+ const end = inner.indexOf("}", i + 1);
2480
+ if (end < 0) return void 0;
2481
+ const hex = inner.slice(i + 2, end);
2482
+ if (!/^[0-9a-fA-F]{1,6}$/.test(hex)) return void 0;
2483
+ out += String.fromCodePoint(parseInt(hex, 16));
2484
+ i = end;
2485
+ } else {
2486
+ const hex = inner.slice(i + 1, i + 5);
2487
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return void 0;
2488
+ out += String.fromCharCode(parseInt(hex, 16));
2489
+ i += 4;
2490
+ }
2491
+ break;
2492
+ }
2493
+ case "\n":
2494
+ break;
2495
+ // 行连接
2496
+ case "\r":
2497
+ if (inner[i + 1] === "\n") i++;
2498
+ break;
2499
+ default:
2500
+ out += n;
2501
+ break;
2502
+ }
2503
+ }
2504
+ return out;
2505
+ }
2506
+ function parseScalarArgAbs(s) {
2507
+ const t = s.trim();
2508
+ if (t === "") return void 0;
2509
+ if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
2510
+ if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
2511
+ if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
2512
+ if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: void 0 }, conf: "exact" };
2513
+ const num2 = parseNumericLiteral(t);
2514
+ if (num2 !== void 0) {
2515
+ return typeof num2 === "bigint" ? bigintLit(num2) : numLit(num2);
2516
+ }
2517
+ const str2 = parseStringLiteralValue(t);
2518
+ if (str2 !== void 0) {
2519
+ return {
2520
+ shape: { k: "prim", type: "string" },
2521
+ term: { op: "lit", value: str2 },
2522
+ conf: "exact"
2523
+ };
2524
+ }
2525
+ return void 0;
2526
+ }
2527
+ function splitCaseArgsText(text) {
2528
+ const result = [];
2529
+ let current = "";
2530
+ let depth = 0;
2531
+ let inString = null;
2532
+ for (let i = 0; i < text.length; i++) {
2533
+ const ch = text[i];
2534
+ if (inString) {
2535
+ current += ch;
2536
+ if (ch === inString) inString = null;
2537
+ continue;
2538
+ }
2539
+ if (ch === '"' || ch === "'") {
2540
+ inString = ch;
2541
+ current += ch;
2542
+ continue;
2543
+ }
2544
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
2545
+ if (ch === ")" || ch === "]" || ch === "}") depth--;
2546
+ if (ch === "," && depth === 0) {
2547
+ result.push(current.trim());
2548
+ current = "";
2549
+ continue;
2550
+ }
2551
+ current += ch;
2552
+ }
2553
+ if (current.trim()) result.push(current.trim());
2554
+ return result;
2555
+ }
2556
+ function stripCaseContinuation(text) {
2557
+ return text.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n");
2558
+ }
2559
+ function findTopLevelColon(text) {
2560
+ let found = -1;
2561
+ let depth = 0;
2562
+ let inString = null;
2563
+ for (let i = 0; i < text.length && found < 0; i++) {
2564
+ const ch = text[i];
2565
+ if (inString) {
2566
+ if (ch === inString) inString = null;
2567
+ continue;
2568
+ }
2569
+ if (ch === '"' || ch === "'") {
2570
+ inString = ch;
2571
+ continue;
2572
+ }
2573
+ if (ch === "(" || ch === "[" || ch === "{") {
2574
+ depth++;
2575
+ continue;
2576
+ }
2577
+ if (ch === ")" || ch === "]" || ch === "}") {
2578
+ depth--;
2579
+ continue;
2580
+ }
2581
+ if (ch === ":" && depth === 0) found = i;
2582
+ }
2583
+ return found;
2584
+ }
2585
+ function parseStructureArgAbs(text) {
2586
+ const t = text.trim();
2587
+ if (t.startsWith("{") && t.endsWith("}")) {
2588
+ const body = t.slice(1, -1).trim();
2589
+ const slots = {};
2590
+ if (body !== "") {
2591
+ for (const entry of splitCaseArgsText(body)) {
2592
+ if (entry === "") continue;
2593
+ const colon = findTopLevelColon(entry);
2594
+ if (colon < 0) return void 0;
2595
+ const keyText = entry.slice(0, colon).trim();
2596
+ if (keyText === "" || keyText.includes("[")) return void 0;
2597
+ const key = keyText.replace(/^["']|["']$/g, "");
2598
+ if (key === "") return void 0;
2599
+ const value = parseCaseArgAbs(entry.slice(colon + 1));
2600
+ if (!value) return void 0;
2601
+ slots[key] = { value };
2602
+ }
2603
+ }
2604
+ return abs({ k: "obj", slots }, void 0, void 0, "exact");
2605
+ }
2606
+ if (t.startsWith("[") && t.endsWith("]")) {
2607
+ const body = t.slice(1, -1).trim();
2608
+ const elements = [];
2609
+ if (body !== "") {
2610
+ for (const el of splitCaseArgsText(body)) {
2611
+ if (el === "") continue;
2612
+ const value = parseCaseArgAbs(el);
2613
+ if (!value) return void 0;
2614
+ elements.push(value);
2615
+ }
2616
+ }
2617
+ return abs({ k: "tuple", elements }, void 0, void 0, "exact");
2618
+ }
2619
+ return void 0;
2620
+ }
2621
+ function parseCaseArgAbs(text) {
2622
+ return parseScalarArgAbs(text) ?? parseStructureArgAbs(text);
2623
+ }
2624
+ function isConstraintBuilderArg(text) {
2625
+ return CONSTRAINT_EXPR_RE.test(text.trim());
2626
+ }
2627
+ function derefDeepConstraint(c) {
2628
+ let cur = c;
2629
+ for (let i = 0; i < LAZY_TEMPLATE_DEPTH && cur.lazy; i++) {
2630
+ cur = derefConstraint(cur);
2631
+ }
2632
+ return cur;
2633
+ }
2634
+ function witnessNoInfo(w) {
2635
+ const k = w.shape.k;
2636
+ return k === "never" || (k === "unknown" || k === "any") && !w.term;
2637
+ }
2638
+ function witnessIsStructure(w) {
2639
+ const k = w.shape.k;
2640
+ return k === "obj" || k === "arr" || k === "tuple";
2641
+ }
2642
+ function witnessPrimType(w) {
2643
+ const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
2644
+ if (shape2.k === "prim") return shape2.type;
2645
+ return void 0;
2646
+ }
2647
+ function constraintIsAnyLike(c) {
2648
+ return c.prim === void 0 && c.preds.length === 0 && (c.members?.length ?? 0) === 0 && !c.fields && !c.element && !c.fn;
2649
+ }
2650
+ function witnessViolations(c, w, path, depth) {
2651
+ if (depth > 6) return [];
2652
+ const cc = derefDeepConstraint(c);
2653
+ if (cc.members && cc.members.length > 0) {
2654
+ let first = [];
2655
+ for (const m of cc.members) {
2656
+ const vs = witnessViolations(m, w, path, depth + 1);
2657
+ if (vs.length === 0) return [];
2658
+ if (first.length === 0) first = vs;
2659
+ }
2660
+ return first;
2661
+ }
2662
+ if (cc.fields) {
2663
+ const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
2664
+ if (shape2.k !== "obj") {
2665
+ if (witnessNoInfo(w)) return [];
2666
+ return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
2667
+ }
2668
+ const out = [];
2669
+ for (const [key, field] of Object.entries(cc.fields)) {
2670
+ const slot = getSlot(shape2.slots, key);
2671
+ const fieldPath = path === "" ? key : `${path}.${key}`;
2672
+ if (!slot) {
2673
+ if (!field.optional && !field.constraint.isOptional) {
2674
+ out.push({
2675
+ actual: w,
2676
+ expected: `missing field ${fieldPath}`,
2677
+ path: fieldPath,
2678
+ missing: true
2679
+ });
2680
+ }
2681
+ continue;
2682
+ }
2683
+ out.push(...witnessViolations(field.constraint, slot.value, fieldPath, depth + 1));
2684
+ }
2685
+ return out;
2686
+ }
2687
+ if (cc.element) {
2688
+ const shape2 = w.shape.k === "brand" ? w.shape.shape.shape : w.shape;
2689
+ const elements = shape2.k === "tuple" ? shape2.elements : shape2.k === "arr" ? [shape2.element] : void 0;
2690
+ if (!elements) {
2691
+ if (witnessNoInfo(w)) return [];
2692
+ return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
2693
+ }
2694
+ const out = [];
2695
+ elements.forEach((el, i) => {
2696
+ out.push(...witnessViolations(cc.element, el, `${path}[${i}]`, depth + 1));
2697
+ });
2698
+ return out;
2699
+ }
2700
+ if (cc.fn) return [];
2701
+ if (witnessIsStructure(w)) {
2702
+ if (constraintIsAnyLike(cc)) return [];
2703
+ return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
2704
+ }
2705
+ const argLit = w.term?.op === "lit" ? w.term.value : void 0;
2706
+ if (w.term?.op !== "lit") return [];
2707
+ if (typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
2708
+ return [];
2709
+ }
2710
+ if (cc.prim) {
2711
+ const prim = witnessPrimType(w);
2712
+ if (prim !== void 0 && prim !== cc.prim) {
2713
+ return [
2714
+ { actual: w, expected: `typeof ${path} = "${cc.prim}"`, path, missing: false }
2715
+ ];
2716
+ }
2717
+ }
2718
+ if (!literalMeetsConstraint(argLit, cc)) {
2719
+ return [{ actual: w, expected: formatConstraint(cc), path, missing: false }];
2720
+ }
2721
+ return [];
2722
+ }
1964
2723
  function projectCaseArgField(arg, fieldPath) {
1965
2724
  let cur = arg;
1966
2725
  for (const seg of fieldPath) {
@@ -1980,57 +2739,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
1980
2739
  const hasContractOrigin = directiveSrc.includes("@nudo:contract") || opts.sidecarPresent === true;
1981
2740
  if (!hasContractOrigin) return out;
1982
2741
  const file = opts.file ?? parseSource(source);
1983
- const parseLitArg = (s) => {
1984
- const t = s.trim();
1985
- if (t === "") return void 0;
1986
- if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
1987
- if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
1988
- if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
1989
- if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: void 0 }, conf: "exact" };
1990
- if (/^-?\d+(\.\d+)?$/.test(t)) return numLit(Number(t));
1991
- const str2 = t.match(/^(['"])([\s\S]*)\1$/);
1992
- if (str2) {
1993
- return {
1994
- shape: { k: "prim", type: "string" },
1995
- term: { op: "lit", value: str2[2] },
1996
- conf: "exact"
1997
- };
1998
- }
1999
- return void 0;
2000
- };
2001
- const parseCaseArgs = (raw) => {
2002
- const m = raw.match(/@nudo:case\s+"[^"]+"\s*\(([\s\S]*)\)/);
2003
- if (!m) return void 0;
2004
- const inner = m[1].trim();
2005
- if (inner === "") return [];
2006
- const parts = [];
2007
- let depth = 0;
2008
- let cur = "";
2009
- let quote = null;
2010
- for (let i = 0; i < inner.length; i++) {
2011
- const ch = inner[i];
2012
- if (quote) {
2013
- cur += ch;
2014
- if (ch === quote && inner[i - 1] !== "\\") quote = null;
2015
- continue;
2016
- }
2017
- if (ch === '"' || ch === "'") {
2018
- quote = ch;
2019
- cur += ch;
2020
- continue;
2021
- }
2022
- if (ch === "(" || ch === "[" || ch === "{") depth++;
2023
- if (ch === ")" || ch === "]" || ch === "}") depth--;
2024
- if (ch === "," && depth === 0) {
2025
- parts.push(cur);
2026
- cur = "";
2027
- continue;
2028
- }
2029
- cur += ch;
2030
- }
2031
- if (cur.trim()) parts.push(cur);
2032
- return parts;
2033
- };
2034
2742
  const checkCaseAgainstReqs = (fnName, caseName, args, line) => {
2035
2743
  const g = generalizeFromAst(fnName, source, {
2036
2744
  ...file ? { file } : {},
@@ -2072,20 +2780,45 @@ function scanCaseInconsistency(source, knownFns, opts) {
2072
2780
  ]);
2073
2781
  }
2074
2782
  if (reqs.length === 0) return;
2075
- const absArgs = args.map((a) => parseLitArg(a) ?? abs({ k: "unknown" }, void 0, void 0, "partial"));
2783
+ const absArgs = args.map((a) => parseCaseArgAbs(a));
2076
2784
  for (const req of reqs) {
2077
2785
  const idx = req[0];
2078
2786
  const entry = req[1];
2079
2787
  const fieldPath = req[2];
2080
- if (entry.constraint.fields) continue;
2788
+ const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2789
+ if (idx >= args.length) {
2790
+ out.push({
2791
+ severity: "warning",
2792
+ code: DIAG.directiveSyntax,
2793
+ message: `${fnName} case "${caseName}": no witness argument for contract slot ${paramName} (${args.length} argument(s) given)`,
2794
+ actual: `${args.length} argument(s)`,
2795
+ expected: `${idx + 1} argument(s)`,
2796
+ suggestion: `add a witness argument for ${paramName}, or drop the contract on ${paramName}`,
2797
+ fn: fnName,
2798
+ line
2799
+ });
2800
+ continue;
2801
+ }
2802
+ if (absArgs[idx] === void 0) {
2803
+ if (isConstraintBuilderArg(args[idx])) continue;
2804
+ out.push({
2805
+ severity: "warning",
2806
+ code: DIAG.directiveSyntax,
2807
+ message: `${fnName} case "${caseName}": witness argument ${idx + 1} could not be parsed \u2014 contract slot ${paramName} was not checked`,
2808
+ actual: args[idx],
2809
+ expected: "a literal witness (number / string / boolean / null / undefined / object literal / array literal)",
2810
+ suggestion: `use a literal witness for ${paramName}, or relax the refine on ${paramName}`,
2811
+ fn: fnName,
2812
+ line
2813
+ });
2814
+ continue;
2815
+ }
2081
2816
  let arg = absArgs[idx];
2082
- if (!arg) continue;
2083
2817
  if (fieldPath) {
2084
2818
  const projected = projectCaseArgField(arg, fieldPath);
2085
2819
  if (!projected) {
2086
2820
  const k = arg.shape.k;
2087
2821
  if (k !== "unknown" && k !== "any") {
2088
- const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2089
2822
  const field = fieldPath.join(".");
2090
2823
  out.push({
2091
2824
  severity: "error",
@@ -2102,6 +2835,22 @@ function scanCaseInconsistency(source, knownFns, opts) {
2102
2835
  }
2103
2836
  arg = projected;
2104
2837
  }
2838
+ const cc = derefDeepConstraint(entry.constraint);
2839
+ if (cc.fields || witnessIsStructure(arg)) {
2840
+ for (const v2 of witnessViolations(cc, arg, "", 0)) {
2841
+ out.push({
2842
+ severity: "error",
2843
+ code: DIAG.caseInconsistency,
2844
+ message: `${fnName} case "${caseName}": witness \u22AD contract`,
2845
+ actual: formatAbs(v2.actual),
2846
+ expected: v2.expected,
2847
+ suggestion: v2.missing ? `add the missing field ${v2.path} to the case argument (contract slot ${paramName})` : `change the case argument, or relax the refine on ${paramName}`,
2848
+ fn: fnName,
2849
+ line
2850
+ });
2851
+ }
2852
+ continue;
2853
+ }
2105
2854
  const argLit = arg.term?.op === "lit" ? arg.term.value : void 0;
2106
2855
  const argIsLit = arg.term?.op === "lit";
2107
2856
  if (!argIsLit || typeof argLit !== "number" && typeof argLit !== "string" && typeof argLit !== "boolean" && argLit !== null && argLit !== void 0) {
@@ -2110,7 +2859,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
2110
2859
  const hasEqOr = (entry.constraint.members?.length ?? 0) > 0 || entry.constraint.preds.some((p) => p.op === "eq");
2111
2860
  if (hasEqOr) {
2112
2861
  if (!literalMeetsConstraint(argLit, entry.constraint)) {
2113
- const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2114
2862
  out.push({
2115
2863
  severity: "error",
2116
2864
  code: DIAG.caseInconsistency,
@@ -2126,7 +2874,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
2126
2874
  }
2127
2875
  if (typeof argLit !== "number") continue;
2128
2876
  if (isIntFlag(entry.constraint) && !Number.isInteger(argLit)) {
2129
- const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2130
2877
  out.push({
2131
2878
  severity: "error",
2132
2879
  code: DIAG.caseInconsistency,
@@ -2148,7 +2895,6 @@ function scanCaseInconsistency(source, knownFns, opts) {
2148
2895
  if (p.op === "lt") ok = argLit < n;
2149
2896
  if (p.op === "le") ok = argLit <= n;
2150
2897
  if (!ok) {
2151
- const paramName = entry.param || paramNames[idx] || `arg${idx}`;
2152
2898
  out.push({
2153
2899
  severity: "error",
2154
2900
  code: DIAG.caseInconsistency,
@@ -2170,8 +2916,7 @@ function scanCaseInconsistency(source, knownFns, opts) {
2170
2916
  const text = scope.commentTexts[ci];
2171
2917
  const startLine = scope.commentStartLines[ci] ?? 0;
2172
2918
  for (const tag of scanCaseTags(text)) {
2173
- const caseArgs = parseCaseArgs(`@nudo:case "${tag.name}" (${tag.argsText})`);
2174
- if (!caseArgs) continue;
2919
+ const caseArgs = splitCaseArgsText(stripCaseContinuation(tag.argsText));
2175
2920
  checkCaseAgainstReqs(scope.name, tag.name, caseArgs, startLine);
2176
2921
  }
2177
2922
  }
@@ -2187,18 +2932,27 @@ function checkSource(filePath, source, phi = pTrue, opts = {}) {
2187
2932
  }
2188
2933
  function checkSourceInScope(filePath, source, phi, opts) {
2189
2934
  let callOpts = opts;
2935
+ const loadReadFailures = [];
2936
+ const loadFailureKeys = /* @__PURE__ */ new Set();
2937
+ let speculativeLoad;
2190
2938
  if (opts.loadModule && opts.fromFile) {
2191
2939
  const loadCache = /* @__PURE__ */ new Map();
2192
2940
  const raw = opts.loadModule;
2193
- const fromFile = opts.fromFile;
2941
+ const cachedLoad = (spec, from) => {
2942
+ const k = `${from}\0${spec}`;
2943
+ if (!loadCache.has(k)) loadCache.set(k, raw(spec, from));
2944
+ return loadCache.get(k);
2945
+ };
2194
2946
  callOpts = {
2195
2947
  ...opts,
2196
- loadModule: (spec, from) => {
2197
- const k = `${from}\0${spec}`;
2198
- if (!loadCache.has(k)) loadCache.set(k, raw(spec, from));
2199
- return loadCache.get(k);
2200
- }
2948
+ loadModule: guardLoadModule(cachedLoad, (failure) => {
2949
+ const key = `${failure.path}\0${failure.code}`;
2950
+ if (loadFailureKeys.has(key)) return;
2951
+ loadFailureKeys.add(key);
2952
+ loadReadFailures.push(failure);
2953
+ })
2201
2954
  };
2955
+ speculativeLoad = cachedLoad;
2202
2956
  }
2203
2957
  setRefineDiagCollector(null);
2204
2958
  setInterfaceDiagCollector(null);
@@ -2208,11 +2962,20 @@ function checkSourceInScope(filePath, source, phi, opts) {
2208
2962
  const useMemo = phi.op === "true";
2209
2963
  let memoKey;
2210
2964
  const stable = stableAnalyzeKeySource(source);
2211
- const depsFp = checkDepsFingerprint(stable, callOpts);
2212
- const sidecarFp = callOpts.loadModule && callOpts.fromFile ? sidecarClosureFingerprint(callOpts.fromFile, callOpts) : void 0;
2965
+ const depsFp = callOpts.loadModule && callOpts.fromFile ? loadModuleDepsFingerprint(stable, callOpts.loadModule, callOpts.fromFile, {
2966
+ ...speculativeLoad ? { ambientLoadModule: speculativeLoad } : {},
2967
+ ambientAllowed: (sidecarPath) => sidecarAutoBindAllowed(sidecarPath, opts.autoBind, opts.projectDir)
2968
+ }) : { fp: "-", paths: [], contents: [], truncated: false, readError: false };
2969
+ const sidecarFp = callOpts.loadModule && callOpts.fromFile ? sidecarClosureFingerprint(callOpts.fromFile, {
2970
+ ...callOpts,
2971
+ ...speculativeLoad ? { loadModule: speculativeLoad } : {}
2972
+ }) : void 0;
2213
2973
  const allowMemo = useMemo && !depsFp.truncated && // 读错误轮键不可信(#135):错误轮报告(nudo:interface-load)与真 miss 轮
2214
2974
  // (干净)若共享 miss 键会互为跨次陈旧命中——不读也不写 memo
2215
- !depsFp.readError && !(sidecarFp?.startsWith("trunc:") ?? false);
2975
+ !depsFp.readError && // Bug 499:本闸把读失败降级成 miss(指纹面看不到 readError)——但
2976
+ // 「不可读」轮与「真不存在」轮的报告不同(前者多一条读失败诊断),
2977
+ // 共用同一键会互为陈旧命中 ⇒ 读失败轮一律不碰 memo。
2978
+ loadReadFailures.length === 0 && !(sidecarFp?.startsWith("trunc:") ?? false);
2216
2979
  if (allowMemo) {
2217
2980
  memoKey = checkMemoKey(filePath, stable, opts, depsFp, sidecarFp);
2218
2981
  const hit = checkMemoGet(memoKey);
@@ -2225,7 +2988,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
2225
2988
  const issues = [];
2226
2989
  const signatures = [];
2227
2990
  const file = parseSource(source);
2228
- const names = listTopFunctions(source, file);
2991
+ const names = [
2992
+ ...listTopFunctions(source, file),
2993
+ ...listClassEntryNames(source, file)
2994
+ ];
2229
2995
  const memoPaths = sidecarFp !== void 0 && callOpts.fromFile ? [.../* @__PURE__ */ new Set([...depsFp.paths, normPath(sidecarPathOf(callOpts.fromFile))])] : depsFp.paths;
2230
2996
  const truncated = /* @__PURE__ */ new Map();
2231
2997
  resetAbsCallBudget();
@@ -2239,7 +3005,7 @@ function checkSourceInScope(filePath, source, phi, opts) {
2239
3005
  makeSameFileFaceResolver(callOpts, filePath, source, file)
2240
3006
  );
2241
3007
  try {
2242
- attachCrossModuleContractFaces(callOpts, filePath);
3008
+ const crossModuleIssues = attachCrossModuleContractFaces(callOpts, filePath);
2243
3009
  const report = checkSourceInner(
2244
3010
  filePath,
2245
3011
  source,
@@ -2258,8 +3024,10 @@ function checkSourceInScope(filePath, source, phi, opts) {
2258
3024
  ...takeRefineDiagsSince(refineSince),
2259
3025
  ...takeInterfaceDiagsSince(ifaceSince)
2260
3026
  ]);
2261
- if (diagIssues.length > 0) {
2262
- report.issues.push(...diagIssues);
3027
+ const loadIssues = loadReadFailureIssues(loadReadFailures);
3028
+ const allIssues = [...diagIssues, ...loadIssues, ...crossModuleIssues];
3029
+ if (allIssues.length > 0) {
3030
+ report.issues.push(...allIssues);
2263
3031
  const errors = report.issues.filter((i) => i.severity === "error").length;
2264
3032
  const warnings = report.issues.filter((i) => i.severity === "warning").length;
2265
3033
  const infos = report.issues.filter((i) => i.severity === "info").length;
@@ -2271,7 +3039,9 @@ function checkSourceInScope(filePath, source, phi, opts) {
2271
3039
  functions: report.summary.functions
2272
3040
  };
2273
3041
  }
2274
- if (memoKey) checkMemoSet(memoKey, report, memoPaths);
3042
+ if (memoKey && loadReadFailures.length === 0) {
3043
+ checkMemoSet(memoKey, report, memoPaths);
3044
+ }
2275
3045
  return cloneCheckReport(report);
2276
3046
  } finally {
2277
3047
  setContractFaceResolver(prevFaceResolver);
@@ -2280,6 +3050,14 @@ function checkSourceInScope(filePath, source, phi, opts) {
2280
3050
  setAbsTruncationCollector(prevTrunc);
2281
3051
  }
2282
3052
  }
3053
+ function loadReadFailureIssues(failures) {
3054
+ return failures.map((f) => ({
3055
+ severity: "error",
3056
+ code: DIAG.moduleUnreadable,
3057
+ message: `cannot read module '${f.path}' (${f.code}): ${f.message}`,
3058
+ suggestion: "fix the file permissions (or the module path) \u2014 the dependency is present but unreadable, so its types are unknown"
3059
+ }));
3060
+ }
2283
3061
  function makeSameFileFaceResolver(opts, filePath, source, file) {
2284
3062
  const loadModule = opts.loadModule;
2285
3063
  const fromFile = opts.fromFile ?? filePath;
@@ -2307,24 +3085,48 @@ function makeSameFileFaceResolver(opts, filePath, source, file) {
2307
3085
  return faces;
2308
3086
  };
2309
3087
  }
3088
+ function unmatchedContractParams(eff, formalDisplay, formals) {
3089
+ const contractNames = contractParamNameSet(formals);
3090
+ const display = new Set(formalDisplay);
3091
+ const conflictNames = new Set(eff.conflict?.params ?? []);
3092
+ return eff.params.map((p) => p.param).filter(
3093
+ (p) => p && !display.has(p) && !contractNames.has(p) && !conflictNames.has(p)
3094
+ );
3095
+ }
3096
+ function paramMismatchIssue(name, unknownParams, formalDisplay, formals) {
3097
+ const contractNames = contractParamNameSet(formals);
3098
+ const surface = [...contractNames].filter((n) => !n.startsWith("_p") && n !== "_").join(", ");
3099
+ return {
3100
+ severity: "error",
3101
+ code: DIAG.interfaceParamMismatch,
3102
+ message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
3103
+ actual: unknownParams.join(", "),
3104
+ expected: surface || formalDisplay.join(", ") || "(no params)",
3105
+ suggestion: `rename the @nudo:contract / sidecar binding parameter to one of: ${surface || formalDisplay.join(", ") || "(function has no params)"}`,
3106
+ fn: name
3107
+ };
3108
+ }
2310
3109
  function attachCrossModuleContractFaces(opts, filePath) {
2311
3110
  const loadModule = opts.loadModule;
2312
3111
  const fromFile = opts.fromFile ?? filePath;
2313
- if (!loadModule) return;
3112
+ if (!loadModule) return [];
2314
3113
  const tables = [];
2315
3114
  for (const t of [opts.modules, opts.inject?.modules]) {
2316
3115
  if (t && typeof t === "object" && !tables.includes(t)) tables.push(t);
2317
3116
  }
2318
- if (tables.length === 0) return;
3117
+ if (tables.length === 0) return [];
2319
3118
  const autoBind = opts.autoBind;
2320
3119
  const projectDir = opts.projectDir;
3120
+ const mismatchIssues = [];
2321
3121
  const facesCache = /* @__PURE__ */ new Map();
2322
3122
  const facesFor = (depSource, depPath, name, paramNames) => {
2323
3123
  const key = `${depPath}\0${name}`;
2324
- if (facesCache.has(key)) return facesCache.get(key);
3124
+ const hit = facesCache.get(key);
3125
+ if (hit) return hit;
2325
3126
  let faces;
3127
+ let eff;
2326
3128
  try {
2327
- const eff = effectiveInterface(depSource, name, {
3129
+ eff = effectiveInterface(depSource, name, {
2328
3130
  loadModule,
2329
3131
  fromFile: depPath,
2330
3132
  ...autoBind !== void 0 ? { autoBind } : {},
@@ -2333,22 +3135,36 @@ function attachCrossModuleContractFaces(opts, filePath) {
2333
3135
  if (eff) faces = contractFacesFromEffectiveInterface(eff, paramNames);
2334
3136
  } catch {
2335
3137
  faces = void 0;
3138
+ eff = void 0;
2336
3139
  }
2337
- facesCache.set(key, faces);
2338
- return faces;
3140
+ const out = { faces, eff };
3141
+ facesCache.set(key, out);
3142
+ return out;
2339
3143
  };
2340
3144
  const attachFnAbs = (v2, depSource, depPath, name) => {
2341
3145
  if (!v2 || typeof v2 !== "object" || !("shape" in v2)) return;
2342
3146
  const shape2 = v2.shape;
2343
3147
  if (shape2.k !== "fn") return;
2344
- let paramNames;
3148
+ let paramNames = [];
3149
+ let formals = [];
2345
3150
  try {
2346
- paramNames = extractFn(depSource, name)?.params ?? [];
3151
+ const meta = extractFn(depSource, name);
3152
+ paramNames = meta?.params ?? [];
3153
+ formals = meta?.formals ?? [];
2347
3154
  } catch {
2348
3155
  paramNames = [];
3156
+ formals = [];
2349
3157
  }
2350
- const faces = facesFor(depSource, depPath, name, paramNames);
3158
+ const { faces, eff } = facesFor(depSource, depPath, name, paramNames);
2351
3159
  if (faces) attachContractFaces(v2, faces);
3160
+ if (eff?.source === "handwritten") {
3161
+ const unknownParams = unmatchedContractParams(eff, paramNames, formals);
3162
+ if (unknownParams.length > 0) {
3163
+ mismatchIssues.push(
3164
+ paramMismatchIssue(name, unknownParams, paramNames, formals)
3165
+ );
3166
+ }
3167
+ }
2352
3168
  };
2353
3169
  for (const table of tables) {
2354
3170
  for (const [spec, mod2] of Object.entries(table)) {
@@ -2370,6 +3186,7 @@ function attachCrossModuleContractFaces(opts, filePath) {
2370
3186
  if (def !== void 0) attachFnAbs(def, depSource, depPath, "default");
2371
3187
  }
2372
3188
  }
3189
+ return mismatchIssues;
2373
3190
  }
2374
3191
  function sidecarDiagIssues(diags) {
2375
3192
  const seen = /* @__PURE__ */ new Set();
@@ -2379,7 +3196,14 @@ function sidecarDiagIssues(diags) {
2379
3196
  if (seen.has(k)) continue;
2380
3197
  seen.add(k);
2381
3198
  const severity = d.code === DIAG.contractSyntax || d.code === DIAG.directiveSyntax ? "warning" : "error";
2382
- out.push({ severity, code: d.code, message: d.message });
3199
+ out.push({
3200
+ severity,
3201
+ code: d.code,
3202
+ message: d.message,
3203
+ // Bug 374:位置透传(nudo:contract-syntax 不再落在文件第 1 行第 1 列)
3204
+ ...d.line !== void 0 ? { line: d.line } : {},
3205
+ ...d.column !== void 0 ? { column: d.column } : {}
3206
+ });
2383
3207
  }
2384
3208
  return out;
2385
3209
  }
@@ -2397,6 +3221,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
2397
3221
  const preds = [];
2398
3222
  const display = [];
2399
3223
  for (const seg of segs) {
3224
+ const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
2400
3225
  if (seg.kind === "bad") {
2401
3226
  issues.push({
2402
3227
  severity: "error",
@@ -2405,7 +3230,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
2405
3230
  actual: seg.raw,
2406
3231
  expected: "<param> <= <param>",
2407
3232
  suggestion: "cross-variable lemmas: @nudo:assume d <= max \u2014 constant bounds: sidecar number().le(10)",
2408
- fn: name
3233
+ fn: name,
3234
+ ...pos
2409
3235
  });
2410
3236
  continue;
2411
3237
  }
@@ -2418,7 +3244,8 @@ function assumePremisesPhiOf(name, source, file, issues) {
2418
3244
  actual: bad.join(", "),
2419
3245
  expected: surface || "(no params/locals)",
2420
3246
  suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
2421
- fn: name
3247
+ fn: name,
3248
+ ...pos
2422
3249
  });
2423
3250
  continue;
2424
3251
  }
@@ -2426,7 +3253,7 @@ function assumePremisesPhiOf(name, source, file, issues) {
2426
3253
  display.push(`${seg.lhs} <= ${seg.rhs}`);
2427
3254
  }
2428
3255
  if (preds.length === 0) return void 0;
2429
- return { phi: and(...preds), display };
3256
+ return { phi: and(...preds), display, premises: preds };
2430
3257
  }
2431
3258
  function invariantSpecsOf(name, source, file, issues) {
2432
3259
  const segs = extractInvariantSegs(source, name);
@@ -2441,6 +3268,7 @@ function invariantSpecsOf(name, source, file, issues) {
2441
3268
  const specs = [];
2442
3269
  const display = [];
2443
3270
  for (const seg of segs) {
3271
+ const pos = seg.line !== void 0 ? { line: seg.line, ...seg.column !== void 0 ? { column: seg.column } : {} } : {};
2444
3272
  if (seg.kind === "bad") {
2445
3273
  issues.push({
2446
3274
  severity: "warning",
@@ -2449,7 +3277,8 @@ function invariantSpecsOf(name, source, file, issues) {
2449
3277
  actual: seg.raw,
2450
3278
  expected: "<name><path> <rel> <number>",
2451
3279
  suggestion: "loop-carried bounds: @nudo:invariant dp[*][*] >= 0 \u2014 scalar: @nudo:invariant last >= 1",
2452
- fn: name
3280
+ fn: name,
3281
+ ...pos
2453
3282
  });
2454
3283
  continue;
2455
3284
  }
@@ -2461,7 +3290,8 @@ function invariantSpecsOf(name, source, file, issues) {
2461
3290
  actual: seg.name,
2462
3291
  expected: surface || "(no params/locals)",
2463
3292
  suggestion: `reference one of: ${surface || "(no params or top-level locals)"}`,
2464
- fn: name
3293
+ fn: name,
3294
+ ...pos
2465
3295
  });
2466
3296
  continue;
2467
3297
  }
@@ -2471,6 +3301,36 @@ function invariantSpecsOf(name, source, file, issues) {
2471
3301
  if (specs.length === 0) return void 0;
2472
3302
  return { specs, display };
2473
3303
  }
3304
+ function dedupeAssignRecords(records) {
3305
+ const seen = /* @__PURE__ */ new Set();
3306
+ const out = [];
3307
+ for (const r of records) {
3308
+ const key = JSON.stringify([
3309
+ r.name,
3310
+ r.line ?? "",
3311
+ r.column ?? "",
3312
+ r.conditional ? "1" : "0",
3313
+ assignValueKey(r.prev),
3314
+ assignValueKey(r.next)
3315
+ ]);
3316
+ if (seen.has(key)) continue;
3317
+ seen.add(key);
3318
+ out.push(r);
3319
+ }
3320
+ return out;
3321
+ }
3322
+ function assignValueKey(v2) {
3323
+ if (v2 === void 0) return "-";
3324
+ if (v2 === null) return "null";
3325
+ if (typeof v2 !== "object") return `host:${typeof v2}:${String(v2)}`;
3326
+ const o = v2;
3327
+ if (!("shape" in o) || !("conf" in o)) return `host:${typeof v2}`;
3328
+ try {
3329
+ return formatAbs(v2);
3330
+ } catch {
3331
+ return `abs:${o.shape?.k ?? "?"}:${o.conf ?? "?"}`;
3332
+ }
3333
+ }
2474
3334
  var OPAQUE_EVIDENCE_HINT = "add @nudo:case or a call site";
2475
3335
  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
3336
  function isBuiltinModuleSpec(spec) {
@@ -2505,9 +3365,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2505
3365
  eiCache.set(key, eff);
2506
3366
  return eff;
2507
3367
  };
3368
+ const records = [];
3369
+ const prevAssign = setEvalAssignCollector((r) => records.push(r));
2508
3370
  for (const name of names) {
2509
3371
  const assumePhi = assumePremisesPhiOf(name, source, file, issues);
2510
3372
  const inv = invariantSpecsOf(name, source, file, issues);
3373
+ resetAssumeConsumed();
2511
3374
  const runSeeded = (body) => {
2512
3375
  if (assumePhi === void 0 && inv === void 0) return body();
2513
3376
  return withAssumePremises(assumePhi?.phi ?? pTrue, () => withInvariantSpecs(inv?.specs ?? [], body));
@@ -2525,6 +3388,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2525
3388
  contractParam.set(p.param, constraintToEntryAbs(p.constraint, p.param));
2526
3389
  }
2527
3390
  }
3391
+ const skipFmt = formatSigCached(retAbs, name);
2528
3392
  signatures.push({
2529
3393
  name,
2530
3394
  params,
@@ -2533,8 +3397,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2533
3397
  return a ? formatShape(a) : "any";
2534
3398
  }),
2535
3399
  abs: retAbs,
2536
- display: formatAbs(retAbs),
2537
- detail: formatAbsMultiline(retAbs, name),
3400
+ display: skipFmt.display,
3401
+ detail: skipFmt.detail,
2538
3402
  conf: retAbs.conf,
2539
3403
  ...isEntryFn ? { entry: true } : {}
2540
3404
  });
@@ -2544,12 +3408,29 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2544
3408
  fromFile: refineFrom
2545
3409
  });
2546
3410
  const display = named?.name ?? formatConstraint(eff.returns.constraint);
2547
- returnContracts.set(name, { display, constraint: eff.returns.constraint });
3411
+ const contractSource = named ? "directive" : "sidecar";
3412
+ returnContracts.set(name, {
3413
+ display,
3414
+ constraint: eff.returns.constraint,
3415
+ source: contractSource
3416
+ });
2548
3417
  const retIssues = checkReturnConstraint(
2549
3418
  name,
2550
3419
  display,
2551
3420
  eff.returns.constraint,
2552
- declared
3421
+ declared,
3422
+ {
3423
+ // #369 / #372:issue 构造处带函数声明位置
3424
+ loc: findFnLoc(file, name),
3425
+ source: contractSource,
3426
+ // Bug 1094:返回点的路径前提 Φ(用户引理前提)——skip 分支同样
3427
+ // 透传(函数级公理在 skip 面也成立;证明通道按「同项 ∧ 蕴含」
3428
+ // 口径消费 ⇒ 不会凭空制造确定违约)。assumeDisplay 一并带上 ⇒
3429
+ // Φ 被消费时归因(nudo:assume-relied)仍然可见(不得静默消费)。
3430
+ phi: assumePhi?.phi,
3431
+ phiPremises: assumePhi?.premises,
3432
+ assumeDisplay: assumePhi?.display
3433
+ }
2553
3434
  );
2554
3435
  if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
2555
3436
  issues.push(...retIssues);
@@ -2587,7 +3468,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2587
3468
  };
2588
3469
  const effects = collectEntryMayThrows(source, name, synthetic, file, phi, opts);
2589
3470
  const throwsDisplayFallback = formatThrowsAbs(mayThrowEffectsToAbs(effects));
2590
- const declared = extractDeclaredThrows(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
3471
+ const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
2591
3472
  const remaining = filterGateThrows(effects, declared, ignoreThrows);
2592
3473
  const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
2593
3474
  if (gateDisplay && remaining.length > 0) {
@@ -2632,7 +3513,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2632
3513
  if (isEntry && entryThrowsMode !== "off") {
2633
3514
  const effects = runSeeded(() => collectEntryMayThrows(source, name, g, file, phi, opts));
2634
3515
  throwsDisplay = formatThrowsAbs(mayThrowEffectsToAbs(effects));
2635
- const declared = extractDeclaredThrows(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
3516
+ const declared = declaredThrowsForEntry(source, name) ?? effectiveInterfaceCached(name)?.throws?.kinds;
2636
3517
  const remaining = filterGateThrows(effects, declared, ignoreThrows);
2637
3518
  const gateDisplay = formatThrowsAbs(mayThrowEffectsToAbs(remaining));
2638
3519
  if (gateDisplay && remaining.length > 0) {
@@ -2658,7 +3539,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2658
3539
  const paramTypes = g.typeParams.map((t) => {
2659
3540
  if (!t.value) return "any";
2660
3541
  if (t.value.shape.k === "unknown") return "unknown";
2661
- return formatShape(t.value);
3542
+ return formatEntryParamShape(t.value);
2662
3543
  });
2663
3544
  signatures.push({
2664
3545
  name,
@@ -2678,7 +3559,12 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2678
3559
  // 的排除一致。
2679
3560
  (t) => t.value && t.value.shape.k === "unknown" && t.value.term?.op !== "lit"
2680
3561
  );
2681
- const budgetExplained = truncated.has(name) || g.symbolic?.conf === "opaque" || g.symbolic?.shape?.k === "sum" && g.symbolic.shape.members.every((m) => m.shape.k === "unknown" || m.shape.k === "any");
3562
+ const budgetExplained = truncated.has(name) || // Bug 389:`conf=opaque` 通常意味着「路径没覆盖」⇒ 已解释;但**因 env
3563
+ // 门禁而 opaque**(源码声明了 `@nudo:env` 而那个 env 没解析出全局 ⇒
3564
+ // 函数体 fail-closed)不是「已解释」——那正是「推断没成功」,唯一能提示
3565
+ // 它的 `nudo:unknown-inference` 不能被吞(否则用户把 env 名写错的那一刻,
3566
+ // 唯一的失败信号恰好消失)。
3567
+ g.symbolic?.conf === "opaque" && !g.envGated || g.symbolic?.shape?.k === "sum" && g.symbolic.shape.members.every((m) => m.shape.k === "unknown" || m.shape.k === "any");
2682
3568
  const symbolicTrueUnknown = g.symbolic?.shape?.k === "unknown" && g.symbolic.term?.op !== "lit";
2683
3569
  if ((symbolicTrueUnknown || unknownParamIdx >= 0) && !budgetExplained) {
2684
3570
  const where = symbolicTrueUnknown ? `${name} => unknown` : `${name} param ${g.params[unknownParamIdx] ?? `#${unknownParamIdx}`}: unknown`;
@@ -2688,32 +3574,32 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2688
3574
  message: `${name}: signature has true unknown (inference failed)`,
2689
3575
  actual: where,
2690
3576
  expected: "computable Abs (any = unconstrained, unknown = engine debt)",
2691
- suggestion: "add @nudo:case / env mock / refine, or confirm the body is algebraically evaluable",
3577
+ // Bug 1062(族 46):本诊断此前把「`@nudo:replace` 的目标文本没命中」
3578
+ // 说成「推断精度不够」⇒ 归因错误 ⇒ 用户只看到 unknown-inference。
3579
+ // service 侧已加「点名目标文本」的显式诊断(`nudo:directive-syntax`),
3580
+ // 这里补一句归因提示,把「指令没生效」这条可能性点出来。
3581
+ suggestion: "add @nudo:case / env mock / refine, or confirm the body is algebraically evaluable; if you used @nudo:replace, check the target text matches the source",
2692
3582
  fn: name
2693
3583
  });
2694
3584
  }
2695
3585
  if (hasRefineDirective || (exportedNames?.has(name) ?? false)) {
2696
3586
  const eff = effectiveInterfaceCached(name);
2697
3587
  if (eff?.source === "handwritten") {
2698
- const contractNames = contractParamNameSet(g.formals ?? []);
2699
- const formalDisplay = new Set(g.params);
2700
- const conflictNames = new Set(eff.conflict?.params ?? []);
2701
- const unknownParams = eff.params.map((p) => p.param).filter(
2702
- (p) => p && !formalDisplay.has(p) && !contractNames.has(p) && !conflictNames.has(p)
2703
- );
3588
+ const unknownParams = unmatchedContractParams(eff, g.params, g.formals ?? []);
2704
3589
  if (unknownParams.length > 0) {
2705
- const surface = [...contractNames].filter((n) => !n.startsWith("_p") && n !== "_").join(", ");
2706
- issues.push({
2707
- severity: "error",
2708
- code: DIAG.interfaceParamMismatch,
2709
- message: `${name}: contract parameter name(s) not in the formal parameter list (${unknownParams.join(", ")})`,
2710
- actual: unknownParams.join(", "),
2711
- expected: surface || g.params.join(", ") || "(no params)",
2712
- suggestion: `rename the @nudo:contract / sidecar binding parameter to one of: ${surface || g.params.join(", ") || "(function has no params)"}`,
2713
- fn: name
2714
- });
3590
+ issues.push(
3591
+ paramMismatchIssue(name, unknownParams, g.params, g.formals ?? [])
3592
+ );
2715
3593
  }
2716
3594
  }
3595
+ const contractLocs = contractSegmentLocs(source, name);
3596
+ const locOf = (param) => {
3597
+ const hit = contractLocs.find((c) => {
3598
+ const head = c.segment.trim().split(/\s+/)[0];
3599
+ return head === param;
3600
+ });
3601
+ return hit?.loc ? { line: hit.loc.line, ...hit.loc.column !== void 0 ? { column: hit.loc.column } : {} } : {};
3602
+ };
2717
3603
  if (eff?.conflict) {
2718
3604
  if (eff.conflict.params.length > 0) {
2719
3605
  issues.push({
@@ -2721,7 +3607,8 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2721
3607
  code: DIAG.interfaceConflict,
2722
3608
  message: `${name}: handwritten contract conjunction unsatisfiable (${eff.conflict.params.join(", ")})`,
2723
3609
  suggestion: "check whether the source @nudo:contract and the same-name sidecar binding have contradictory constant bounds",
2724
- fn: name
3610
+ fn: name,
3611
+ ...locOf(eff.conflict.params[0])
2725
3612
  });
2726
3613
  }
2727
3614
  if (eff.conflict.returns) {
@@ -2730,24 +3617,70 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2730
3617
  code: DIAG.interfaceConflict,
2731
3618
  message: `${name}: handwritten contract conjunction unsatisfiable on the return slot`,
2732
3619
  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)",
2733
- fn: name
3620
+ fn: name,
3621
+ ...locOf("return")
2734
3622
  });
2735
3623
  }
2736
3624
  }
3625
+ const undecidable = eff?.undecidable;
3626
+ if (undecidable !== void 0 && undecidable.params.length > 0) {
3627
+ issues.push({
3628
+ severity: "warning",
3629
+ code: DIAG.interfaceConflict,
3630
+ message: `${name}: contract conjunction not computable (${undecidable.params.join(", ")}) \u2014 only one side is enforced`,
3631
+ suggestion: "structural contracts (shape / array / union / fn) cannot be combined in this slot; the other side's obligations are NOT enforced \u2014 merge them by hand",
3632
+ fn: name,
3633
+ ...locOf(undecidable.params[0])
3634
+ });
3635
+ }
3636
+ if (undecidable?.returns) {
3637
+ issues.push({
3638
+ severity: "warning",
3639
+ code: DIAG.interfaceConflict,
3640
+ message: `${name}: contract conjunction not computable on the return slot \u2014 only one side is enforced`,
3641
+ suggestion: "structural return constraints cannot be combined in this slot; the other side's obligations are NOT enforced \u2014 merge them by hand",
3642
+ fn: name,
3643
+ ...locOf("return")
3644
+ });
3645
+ }
2737
3646
  if (eff && eff.source === "handwritten" && eff.returns && !eff.conflict?.returns) {
2738
3647
  const named = extractRefineReturnFromSource(source, name, {
2739
3648
  loadModule: refineLoad,
2740
3649
  fromFile: refineFrom
2741
3650
  });
2742
3651
  const display = named?.name ?? formatConstraint(eff.returns.constraint);
2743
- returnContracts.set(name, { display, constraint: eff.returns.constraint });
3652
+ const contractSource = named ? "directive" : "sidecar";
3653
+ returnContracts.set(name, {
3654
+ display,
3655
+ constraint: eff.returns.constraint,
3656
+ source: contractSource
3657
+ });
3658
+ const consumedAssume = assumePhi ? new Set(
3659
+ assumePhi.display.filter(
3660
+ (_, i) => assumeConsumed().includes(assumePhi.premises[i])
3661
+ )
3662
+ ) : void 0;
2744
3663
  const retIssues = checkReturnConstraint(
2745
3664
  name,
2746
3665
  display,
2747
3666
  eff.returns.constraint,
2748
3667
  g.symbolic,
2749
- assumePhi?.display,
2750
- inv?.display
3668
+ {
3669
+ // #369 / #372:issue 构造处带函数声明位置
3670
+ loc: findFnLoc(file, name),
3671
+ source: contractSource,
3672
+ assumeDisplay: assumePhi?.display,
3673
+ invariantDisplay: inv?.display,
3674
+ consumedAssume,
3675
+ // Bug 1094:返回点的**路径前提 Φ** = 用户引理前提
3676
+ //(`@nudo:assume` 的前提 Φ)——证明通道的 Φ 侧。
3677
+ // 项身份:前提项用**真实形参名**(`termVar(name)`),泛化后的返回
3678
+ // 项在形参**有契约**时同样锚在真实名上(`constraintToEntryAbs`)
3679
+ // ⇒ 同项判定能对上;无契约时返回项锚在 α 名(A1…)⇒ 对不上 ⇒
3680
+ // Φ 侧不生效(保守少证,不假红)。
3681
+ phi: assumePhi?.phi,
3682
+ phiPremises: assumePhi?.premises
3683
+ }
2751
3684
  );
2752
3685
  if (retIssues.some((i) => i.severity === "error")) returnViolated.add(name);
2753
3686
  issues.push(...retIssues);
@@ -2760,7 +3693,7 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2760
3693
  });
2761
3694
  }
2762
3695
  }
2763
- if (g.symbolic.conf === "opaque" && !truncated.has(name)) {
3696
+ if (g.symbolic.conf === "opaque" && !truncated.has(name) && !g.envGated) {
2764
3697
  issues.push({
2765
3698
  severity: "info",
2766
3699
  code: DIAG.opaqueResult,
@@ -2839,16 +3772,19 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2839
3772
  fn: label
2840
3773
  });
2841
3774
  }
2842
- const records = [];
2843
3775
  const varAbs = /* @__PURE__ */ new Map();
2844
3776
  const callRecords = [];
2845
3777
  const bCalls = [];
2846
- const prevAssign = setEvalAssignCollector((r) => records.push(r));
2847
3778
  const prevCall = setEvalCallCollector((r) => bCalls.push(r));
2848
3779
  let evalBindings;
2849
3780
  const evalAnalyze = evalAnalyzeOpts(opts);
3781
+ let moduleThrow;
2850
3782
  try {
2851
- const evalRun = tryRunTranspiled(source, evalAnalyze);
3783
+ const evalAttempt = tryRunTranspiledDetailed(source, evalAnalyze);
3784
+ const evalRun = evalAttempt.run;
3785
+ if (evalRun === void 0 && evalAttempt.fallback?.reason === "module-throw") {
3786
+ moduleThrow = definiteModuleThrowOf(file);
3787
+ }
2852
3788
  evalBindings = evalRun ? bindingsOf(evalRun) : void 0;
2853
3789
  if (evalBindings) {
2854
3790
  for (const [k, v2] of evalBindings) {
@@ -2871,6 +3807,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2871
3807
  setEvalCallCollector(prevCall);
2872
3808
  }
2873
3809
  void evalBindings;
3810
+ if (moduleThrow !== void 0 && entryThrowsMode !== "off") {
3811
+ const kind = moduleThrow.kind;
3812
+ if (kind === void 0 || !isThrowsIgnored(kind, ignoreThrows)) {
3813
+ const thrown = kind !== void 0 ? ` ${kind}` : "";
3814
+ issues.push({
3815
+ severity: entryThrowsMode === "warning" ? "warning" : "error",
3816
+ code: DIAG.moduleThrow,
3817
+ message: `module initializer always throws${thrown} \u2014 the module cannot be imported, so every export is unavailable (module not analyzed)`,
3818
+ actual: `throw at line ${moduleThrow.line}`,
3819
+ expected: "module initializer that can complete, or a conditional throw",
3820
+ suggestion: kind !== void 0 ? `guard the throw if the module should be importable; if throwing at load is intended, ignore it with --ignore-throws ${kind} (or --entry-throws off)` : "guard the throw if the module should be importable; --entry-throws off disables this gate",
3821
+ line: moduleThrow.line,
3822
+ column: moduleThrow.column
3823
+ });
3824
+ }
3825
+ }
2874
3826
  const callIssues = canSkipLiteralCallScan(source, file) ? [] : scanLiteralCalls(source, names, phi, {
2875
3827
  loadModule: opts.loadModule,
2876
3828
  // 与 generalize / 返回后置同一 fromFile 口径:ambient 侧车按真实
@@ -2896,15 +3848,22 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2896
3848
  rec.fnName,
2897
3849
  rc.display,
2898
3850
  rc.constraint,
2899
- rec.result
3851
+ rec.result,
3852
+ {
3853
+ // #369 / #372:issue 构造处带函数声明位置(被违约函数的声明处)
3854
+ loc: findFnLoc(file, rec.fnName),
3855
+ // #378:约束来源随 returnContracts 走(源码指令 vs 侧车 fn() 返回槽)
3856
+ source: rc.source
3857
+ }
2900
3858
  );
2901
3859
  if (retIssues.some((i) => i.severity === "error")) returnViolated.add(rec.fnName);
2902
3860
  issues.push(...retIssues);
2903
3861
  }
2904
3862
  }
2905
3863
  if (driftCandidates.length > 0) {
3864
+ const driftRecords = opts.from && opts.from.length > 0 ? [...callRecords, ...opts.from.map((r) => ({ fnName: r.fnName, args: r.args, result: r.result }))] : callRecords;
2906
3865
  issues.push(
2907
- ...interfaceDriftIssues(driftCandidates, callRecords, /* @__PURE__ */ (() => {
3866
+ ...interfaceDriftIssues(driftCandidates, driftRecords, /* @__PURE__ */ (() => {
2908
3867
  let driftRun;
2909
3868
  let driftInit = false;
2910
3869
  return (fnName, args) => {
@@ -2934,7 +3893,16 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2934
3893
  ...opts.inject ? { inject: opts.inject } : {}
2935
3894
  })
2936
3895
  );
2937
- issues.push(...structuralAssignIssues(records));
3896
+ try {
3897
+ issues.push(...structuralAssignIssues(dedupeAssignRecords(records)));
3898
+ } catch (err) {
3899
+ issues.push({
3900
+ severity: "warning",
3901
+ code: DIAG.internal,
3902
+ message: `assign channel check failed: ${err.message}`,
3903
+ suggestion: "this is an internal error; please report it together with the checked source file"
3904
+ });
3905
+ }
2938
3906
  const errors = issues.filter((i) => i.severity === "error").length;
2939
3907
  const warnings = issues.filter((i) => i.severity === "warning").length;
2940
3908
  const infos = issues.filter((i) => i.severity === "info").length;
@@ -2948,60 +3916,6 @@ function checkSourceInner(filePath, source, file, phi, opts, issues, signatures,
2948
3916
  budget: { ...budget }
2949
3917
  };
2950
3918
  }
2951
- function checkReturnConstraint(fnName, cName, constraint, ret, assumeDisplay, invariantDisplay) {
2952
- const proof = assertImplies(ret, constraint);
2953
- if (proof.status === "proved") {
2954
- const relied = [];
2955
- if (assumeDisplay && assumeDisplay.length > 0) {
2956
- relied.push({
2957
- severity: "info",
2958
- code: DIAG.assumeRelied,
2959
- message: `${fnName}: return contract proved via @nudo:assume ${assumeDisplay.join("; ")}`,
2960
- actual: `proof relies on user lemma${assumeDisplay.length > 1 ? "s" : ""}`,
2961
- expected: "engine verifies the obligation, not the lemma itself",
2962
- suggestion: "@nudo:assume is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
2963
- fn: fnName
2964
- });
2965
- }
2966
- if (invariantDisplay && invariantDisplay.length > 0) {
2967
- relied.push({
2968
- severity: "info",
2969
- code: DIAG.invariantRelied,
2970
- message: `${fnName}: return contract proved via @nudo:invariant ${invariantDisplay.join("; ")}`,
2971
- actual: `proof relies on user invariant${invariantDisplay.length > 1 ? "s" : ""}`,
2972
- expected: "engine verifies the obligation, not the loop invariant itself",
2973
- suggestion: "@nudo:invariant is a user-supplied axiom \u2014 falsify it with concrete @nudo:case runs (nudo test)",
2974
- fn: fnName
2975
- });
2976
- }
2977
- return relied;
2978
- }
2979
- const push = (severity, actual, expected, suggestion) => [
2980
- {
2981
- severity,
2982
- code: severity === "error" ? DIAG.constraintViolated : DIAG.unprovenReturn,
2983
- message: `${fnName}: return value \u22AD @nudo:contract return ${cName}`,
2984
- actual,
2985
- expected,
2986
- suggestion,
2987
- fn: fnName
2988
- }
2989
- ];
2990
- if (proof.status === "disproved") {
2991
- return push(
2992
- "error",
2993
- formatAbs(ret),
2994
- proof.reason,
2995
- proof.reason.includes("nullish") ? `use nullable(${formatConstraint(constraint)}) or union(..., lit(null)) to allow nullish returns` : `return a value satisfying ${formatConstraint(constraint)}`
2996
- );
2997
- }
2998
- return push(
2999
- "warning",
3000
- formatAbs(ret),
3001
- formatConstraint(constraint),
3002
- `${proof.reason}; add a precondition, narrow the return expression, or relax the return contract`
3003
- );
3004
- }
3005
3919
  export {
3006
3920
  $add,
3007
3921
  $arguments,