@nudojs/core 1.3.0 → 1.4.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.
@@ -45,7 +45,614 @@ import {
45
45
  withEvalBudgetOverride,
46
46
  withExecPhi,
47
47
  withFnBudgetOverride
48
- } from "./chunk-CKH6GQAI.js";
48
+ } from "./chunk-LFCF6BLE.js";
49
+
50
+ // src/algebra/directive-scan.ts
51
+ function cleanDirectiveLine(raw, kind) {
52
+ let s = raw;
53
+ if (kind === "block") {
54
+ s = s.replace(/^\s*\*\s?/, "");
55
+ } else {
56
+ s = s.replace(/^\//, "");
57
+ }
58
+ return s.trim();
59
+ }
60
+ function commentTextToLines(text, kind) {
61
+ return text.split("\n").map((line) => cleanDirectiveLine(line, kind));
62
+ }
63
+ function commentKind(c) {
64
+ return c.type === "CommentLine" ? "line" : "block";
65
+ }
66
+ function buildScope(name, node, wrapper, comments, wrapperComments) {
67
+ const seen = /* @__PURE__ */ new Set();
68
+ const texts = [];
69
+ const startLines = [];
70
+ const lines = [];
71
+ for (const c of [...wrapperComments ?? [], ...comments ?? []]) {
72
+ if (seen.has(c)) continue;
73
+ seen.add(c);
74
+ texts.push(c.value);
75
+ startLines.push(c.loc?.start?.line ?? 0);
76
+ for (const line of commentTextToLines(c.value, commentKind(c))) lines.push(line);
77
+ }
78
+ const loc = node.loc;
79
+ return {
80
+ name,
81
+ node,
82
+ commentTexts: texts,
83
+ commentLines: lines,
84
+ commentStartLines: startLines,
85
+ startLine: loc?.start?.line ?? 0
86
+ };
87
+ }
88
+ function isFnExpr(n) {
89
+ return !!n && (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression" || n.type === "FunctionDeclaration");
90
+ }
91
+ function keyNameOf(key) {
92
+ if (!key) return void 0;
93
+ const k = key;
94
+ if (k.type === "Identifier") return k.name;
95
+ if (k.type === "StringLiteral" || k.type === "NumericLiteral") return String(k.value);
96
+ return void 0;
97
+ }
98
+ function listFnDirectiveScopes(file) {
99
+ const out = [];
100
+ const pushNamedFn = (name, fnNode, commentNode, wrapper) => {
101
+ out.push(
102
+ buildScope(
103
+ name,
104
+ fnNode,
105
+ wrapper,
106
+ commentNode?.leadingComments,
107
+ wrapper?.leadingComments
108
+ )
109
+ );
110
+ };
111
+ const visit2 = (node, ctx) => {
112
+ if (!node || typeof node !== "object") return;
113
+ const n = node;
114
+ switch (n.type) {
115
+ case "File":
116
+ visit2(n.program, ctx);
117
+ return;
118
+ case "Program": {
119
+ for (const s of n.body ?? []) visit2(s, ctx);
120
+ return;
121
+ }
122
+ case "ExportNamedDeclaration":
123
+ case "ExportDefaultDeclaration": {
124
+ const decl = n.declaration;
125
+ if (!decl) return;
126
+ const wrapCtx = { ...ctx, wrapperNode: node };
127
+ visitWithWrapper(decl, node, wrapCtx);
128
+ return;
129
+ }
130
+ case "FunctionDeclaration": {
131
+ const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
132
+ pushNamedFn(name, node, node, ctx.wrapperNode);
133
+ visit2(n.body, { owner: name });
134
+ return;
135
+ }
136
+ case "FunctionExpression": {
137
+ const name = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
138
+ pushNamedFn(name, node, node, ctx.wrapperNode);
139
+ visit2(n.body, { owner: name });
140
+ return;
141
+ }
142
+ case "ArrowFunctionExpression": {
143
+ const name = ctx.fallbackName ?? "<anonymous>";
144
+ pushNamedFn(name, node, ctx.commentHost ?? node, ctx.wrapperNode);
145
+ visit2(n.body, { owner: name });
146
+ return;
147
+ }
148
+ case "VariableDeclaration": {
149
+ for (const d of n.declarations ?? []) {
150
+ const decl = d;
151
+ const idName = decl.id?.type === "Identifier" ? decl.id.name : void 0;
152
+ if (isFnExpr(decl.init)) {
153
+ const fn2 = decl.init;
154
+ const scope = buildScope(
155
+ idName ?? "<anonymous>",
156
+ fn2,
157
+ ctx.wrapperNode,
158
+ node.leadingComments,
159
+ ctx.wrapperNode?.leadingComments
160
+ );
161
+ out.push(scope);
162
+ visit2(fn2, { owner: idName });
163
+ } else {
164
+ visit2(decl.init, { owner: idName, commentHost: node, wrapperNode: ctx.wrapperNode });
165
+ }
166
+ }
167
+ return;
168
+ }
169
+ case "ClassDeclaration":
170
+ case "ClassExpression": {
171
+ const cname = n.id?.name ?? ctx.fallbackName ?? "<anonymous>";
172
+ if (n.type === "ClassDeclaration") {
173
+ }
174
+ const body = n.body?.body ?? [];
175
+ for (const m of body) visit2(m, { owner: cname });
176
+ return;
177
+ }
178
+ case "ClassMethod":
179
+ case "ClassPrivateMethod":
180
+ case "ObjectMethod": {
181
+ const key = keyNameOf(n.key);
182
+ const owner = ctx.owner;
183
+ const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
184
+ pushNamedFn(name, node, node, void 0);
185
+ visit2(n.body, { owner: name });
186
+ return;
187
+ }
188
+ case "ObjectProperty":
189
+ case "ClassProperty":
190
+ case "ClassPrivateProperty": {
191
+ const key = keyNameOf(n.key);
192
+ const val = n.value ?? n.init;
193
+ if (isFnExpr(val)) {
194
+ const owner = ctx.owner;
195
+ const name = key ? owner ? `${owner}.${key}` : key : "<anonymous>";
196
+ out.push(
197
+ buildScope(
198
+ name,
199
+ val,
200
+ void 0,
201
+ val.leadingComments ?? node.leadingComments,
202
+ void 0
203
+ )
204
+ );
205
+ visit2(val, { owner: name });
206
+ return;
207
+ }
208
+ visit2(val, ctx);
209
+ return;
210
+ }
211
+ case "ReturnStatement":
212
+ case "ThrowStatement":
213
+ case "AwaitExpression":
214
+ case "SpreadElement":
215
+ case "RestElement":
216
+ visit2(n.argument, ctx);
217
+ return;
218
+ case "UnaryExpression":
219
+ case "UpdateExpression":
220
+ visit2(n.argument, ctx);
221
+ return;
222
+ case "BinaryExpression":
223
+ case "LogicalExpression":
224
+ case "AssignmentExpression":
225
+ visit2(n.left, ctx);
226
+ visit2(n.right, ctx);
227
+ return;
228
+ case "MemberExpression":
229
+ case "OptionalMemberExpression":
230
+ visit2(n.object, ctx);
231
+ visit2(n.property, ctx);
232
+ return;
233
+ case "CallExpression":
234
+ case "OptionalCallExpression":
235
+ case "NewExpression": {
236
+ visit2(n.callee, ctx);
237
+ for (const a of n.arguments ?? []) visit2(a, ctx);
238
+ return;
239
+ }
240
+ case "ConditionalExpression":
241
+ visit2(n.test, ctx);
242
+ visit2(n.consequent, ctx);
243
+ visit2(n.alternate, ctx);
244
+ return;
245
+ case "ArrayExpression": {
246
+ for (const e of n.elements ?? []) visit2(e, ctx);
247
+ return;
248
+ }
249
+ case "ObjectExpression": {
250
+ for (const p of n.properties ?? []) visit2(p, ctx);
251
+ return;
252
+ }
253
+ case "SequenceExpression": {
254
+ for (const e of n.expressions ?? []) visit2(e, ctx);
255
+ return;
256
+ }
257
+ case "ParenthesizedExpression":
258
+ visit2(n.expression, ctx);
259
+ return;
260
+ case "ExpressionStatement":
261
+ visit2(n.expression, ctx);
262
+ return;
263
+ case "BlockStatement": {
264
+ for (const s of n.body ?? []) visit2(s, ctx);
265
+ return;
266
+ }
267
+ case "IfStatement":
268
+ visit2(n.test, ctx);
269
+ visit2(n.consequent, ctx);
270
+ visit2(n.alternate, ctx);
271
+ return;
272
+ case "ForStatement":
273
+ visit2(n.init, ctx);
274
+ visit2(n.test, ctx);
275
+ visit2(n.update, ctx);
276
+ visit2(n.body, ctx);
277
+ return;
278
+ case "ForInStatement":
279
+ case "ForOfStatement":
280
+ visit2(n.left, ctx);
281
+ visit2(n.right, ctx);
282
+ visit2(n.body, ctx);
283
+ return;
284
+ case "WhileStatement":
285
+ case "DoWhileStatement":
286
+ visit2(n.test, ctx);
287
+ visit2(n.body, ctx);
288
+ return;
289
+ case "TryStatement":
290
+ visit2(n.block, ctx);
291
+ visit2(n.handler, ctx);
292
+ visit2(n.finalizer, ctx);
293
+ return;
294
+ case "CatchClause":
295
+ visit2(n.body, ctx);
296
+ return;
297
+ case "SwitchStatement":
298
+ visit2(n.discriminant, ctx);
299
+ for (const c of n.cases ?? []) visit2(c, ctx);
300
+ return;
301
+ case "SwitchCase":
302
+ visit2(n.test, ctx);
303
+ for (const s of n.consequent ?? []) visit2(s, ctx);
304
+ return;
305
+ case "LabeledStatement":
306
+ visit2(n.body, ctx);
307
+ return;
308
+ default: {
309
+ for (const key of [
310
+ "body",
311
+ "expression",
312
+ "init",
313
+ "value",
314
+ "argument",
315
+ "declaration",
316
+ "left",
317
+ "right",
318
+ "test",
319
+ "consequent",
320
+ "alternate",
321
+ "block",
322
+ "handler",
323
+ "finalizer",
324
+ "callee",
325
+ "object",
326
+ "property",
327
+ "argument"
328
+ ]) {
329
+ const v2 = n[key];
330
+ if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
331
+ else if (v2 && typeof v2 === "object" && "type" in v2) visit2(v2, ctx);
332
+ }
333
+ for (const key of ["declarations", "properties", "elements", "arguments", "cases", "params"]) {
334
+ const v2 = n[key];
335
+ if (Array.isArray(v2)) v2.forEach((x) => visit2(x, ctx));
336
+ }
337
+ return;
338
+ }
339
+ }
340
+ };
341
+ const visitWithWrapper = (decl, wrapper, ctx) => {
342
+ const d = decl;
343
+ if (d.type === "FunctionDeclaration" || d.type === "FunctionExpression") {
344
+ const name = d.id?.name ?? "default";
345
+ pushNamedFn(name, decl, decl, wrapper);
346
+ visit2(d.body, { owner: name });
347
+ return;
348
+ }
349
+ if (d.type === "ClassDeclaration" || d.type === "ClassExpression") {
350
+ const cname = d.id?.name ?? "<anonymous>";
351
+ const body = d.body?.body ?? [];
352
+ for (const m of body) visit2(m, { owner: cname });
353
+ return;
354
+ }
355
+ if (d.type === "VariableDeclaration") {
356
+ for (const raw of d.declarations ?? []) {
357
+ const vd = raw;
358
+ const idName = vd.id?.type === "Identifier" ? vd.id.name : void 0;
359
+ if (isFnExpr(vd.init)) {
360
+ const fn2 = vd.init;
361
+ out.push(
362
+ buildScope(
363
+ idName ?? "default",
364
+ fn2,
365
+ wrapper,
366
+ d.leadingComments,
367
+ wrapper.leadingComments
368
+ )
369
+ );
370
+ visit2(fn2, { owner: idName });
371
+ } else {
372
+ visit2(vd.init, {
373
+ owner: idName,
374
+ commentHost: decl,
375
+ wrapperNode: wrapper
376
+ });
377
+ }
378
+ }
379
+ return;
380
+ }
381
+ if (d.type === "ArrowFunctionExpression" || d.type === "FunctionExpression") {
382
+ const name = "default";
383
+ out.push(
384
+ buildScope(
385
+ name,
386
+ decl,
387
+ wrapper,
388
+ d.leadingComments,
389
+ wrapper.leadingComments
390
+ )
391
+ );
392
+ visit2(d.body, { owner: name });
393
+ return;
394
+ }
395
+ visit2(decl, { ...ctx, wrapperNode: wrapper });
396
+ };
397
+ for (const stmt of file.program.body) {
398
+ visit2(stmt, {});
399
+ }
400
+ return out;
401
+ }
402
+ function findFnDirectiveScope(file, fnName) {
403
+ return listFnDirectiveScopes(file).find((s) => s.name === fnName);
404
+ }
405
+ function fnDirectiveCommentLines(source, fnName) {
406
+ let file;
407
+ try {
408
+ file = parseSource(source);
409
+ } catch {
410
+ return [];
411
+ }
412
+ return findFnDirectiveScope(file, fnName)?.commentLines ?? [];
413
+ }
414
+ var isPathLikeEnv = (s) => /^[^\s]+$/.test(s) && (s.startsWith("./") || s.startsWith("../") || s.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(s));
415
+ function parseEnvPayload(payload) {
416
+ return payload.split(",").map((e) => e.trim().replace(/^['"]|['"]$/g, "")).filter((e) => e && (/^\w+$/.test(e) || isPathLikeEnv(e)));
417
+ }
418
+ function parseMockModulePayload(payload) {
419
+ const partial2 = payload.match(/^\s*"([^"]+)"\s*\{([^}]+)\}\s*from\s*"([^"]+)"\s*$/);
420
+ if (partial2) {
421
+ const names = partial2[2].split(",").map((n) => n.trim()).filter(Boolean);
422
+ return { source: partial2[1], names, fromPath: partial2[3] };
423
+ }
424
+ const full = payload.match(/^\s*"([^"]+)"\s+from\s*"([^"]+)"\s*$/);
425
+ if (full) {
426
+ return { source: full[1], fromPath: full[2] };
427
+ }
428
+ return void 0;
429
+ }
430
+ function parseNudoImportPayload(payload) {
431
+ const s = payload.trim();
432
+ if (s === "") return void 0;
433
+ const named = s.match(/^\{([^}]+)\}\s*from\s*["']([^"']+)["']$/);
434
+ if (named) {
435
+ const names = named[1].split("\n").map((line) => line.replace(/^\s*\/\/\/?\s?/, "").trim()).join("\n").split(",").map((x) => x.trim()).filter(Boolean).map((x) => x.split(/\s+as\s+/).pop().trim());
436
+ return { names, spec: named[2] };
437
+ }
438
+ const ns = s.match(/^\*\s+as\s+(\w+)\s+from\s*["']([^"']+)["']$/);
439
+ if (ns) {
440
+ return { names: [`*${ns[1]}`], spec: ns[2] };
441
+ }
442
+ if (/^[\w$]+\s+from\b/.test(s) || /^[\w$]+\s*,/.test(s)) return "malformed-default";
443
+ return "malformed";
444
+ }
445
+ var LINE_COMMENT_PREFIX = "(?:^|[^/:])\\/\\/\\/?";
446
+ function fileDirectiveVisibleText(source) {
447
+ return stripStringsKeepComments(source).replace(/\/\*[\s\S]*?\*\//g, " ");
448
+ }
449
+ function extractFileEnvNames(source) {
450
+ const text = fileDirectiveVisibleText(source);
451
+ const re = new RegExp(`${LINE_COMMENT_PREFIX}\\s*@nudo:env\\s+([^\\n*]+)`, "g");
452
+ const out = [];
453
+ let m;
454
+ while (m = re.exec(text)) {
455
+ out.push(...parseEnvPayload(m[1]));
456
+ }
457
+ return out;
458
+ }
459
+ function extractMockModuleRecords(source) {
460
+ const text = fileDirectiveVisibleText(source);
461
+ const re = new RegExp(
462
+ `${LINE_COMMENT_PREFIX}\\s*@nudo:mock-module\\s+([^\\n]+)`,
463
+ "g"
464
+ );
465
+ const out = [];
466
+ let m;
467
+ while (m = re.exec(text)) {
468
+ const rec = parseMockModulePayload(m[1]);
469
+ if (rec) out.push(rec);
470
+ }
471
+ return out;
472
+ }
473
+ function directiveCommentLinesRaw(source) {
474
+ const src = stripStringsKeepComments(source);
475
+ const out = [];
476
+ const lineRe = /(?:^|[^/:])\/\/\/?([^\n]*)/g;
477
+ let m;
478
+ while (m = lineRe.exec(src)) {
479
+ out.push(cleanDirectiveLine(m[1], "line"));
480
+ }
481
+ const blockRe = /\/\*([\s\S]*?)\*\//g;
482
+ while (m = blockRe.exec(src)) {
483
+ out.push(...commentTextToLines(m[1], "block"));
484
+ }
485
+ return out;
486
+ }
487
+ function nudoImportPayloads(source) {
488
+ const lines = directiveCommentLinesRaw(source);
489
+ const out = [];
490
+ for (let i = 0; i < lines.length; i++) {
491
+ const line = lines[i];
492
+ if (!/^@nudo:import\s/.test(line)) continue;
493
+ let payload = line.replace(/^@nudo:import\s+/, "");
494
+ if (payload.startsWith("{")) {
495
+ let depth = 0;
496
+ for (const ch of payload) {
497
+ if (ch === "{") depth++;
498
+ else if (ch === "}") depth--;
499
+ }
500
+ while (depth > 0 && i + 1 < lines.length) {
501
+ i += 1;
502
+ const cont = lines[i];
503
+ if (/^@nudo:/.test(cont)) {
504
+ i -= 1;
505
+ break;
506
+ }
507
+ payload += `
508
+ ${cont}`;
509
+ for (const ch of cont) {
510
+ if (ch === "{") depth++;
511
+ else if (ch === "}") depth--;
512
+ }
513
+ }
514
+ }
515
+ out.push(payload);
516
+ }
517
+ return out;
518
+ }
519
+ function extractNudoImportRecords(source) {
520
+ const out = [];
521
+ for (const payload of nudoImportPayloads(source)) {
522
+ const rec = parseNudoImportPayload(payload);
523
+ if (rec && rec !== "malformed" && rec !== "malformed-default") out.push(rec);
524
+ }
525
+ return out;
526
+ }
527
+ function scanMalformedNudoImports(source) {
528
+ const out = [];
529
+ for (const payload of nudoImportPayloads(source)) {
530
+ const rec = parseNudoImportPayload(payload);
531
+ if (rec === "malformed-default" || rec === "malformed") out.push(rec);
532
+ }
533
+ return out;
534
+ }
535
+ function extractBalancedParens(text, startIdx) {
536
+ if (text[startIdx] !== "(") return null;
537
+ let depth = 0;
538
+ let inString = null;
539
+ for (let i = startIdx; i < text.length; i++) {
540
+ const ch = text[i];
541
+ if (inString) {
542
+ if (ch === inString) inString = null;
543
+ continue;
544
+ }
545
+ if (ch === '"' || ch === "'") {
546
+ inString = ch;
547
+ continue;
548
+ }
549
+ if (ch === "(") depth++;
550
+ if (ch === ")") depth--;
551
+ if (depth === 0) return text.slice(startIdx + 1, i);
552
+ }
553
+ return null;
554
+ }
555
+ var CASE_NAME_REGEX = /(?:^|\n)[ \t]*(?:\*[ \t]*|\/\/\/?\s*)?@nudo:case\s+"([^"]+)"\s*\(/g;
556
+ function extractCaseExpectedExpr(text, afterParen) {
557
+ const lines = text.slice(afterParen).split("\n");
558
+ for (let i = 0; i < lines.length; i++) {
559
+ let line = lines[i];
560
+ if (i > 0) line = line.replace(/^\s*\*\s?/, "").replace(/^\s*\/\/\/?\s?/, "");
561
+ if (/^\s*@nudo:/.test(line)) return void 0;
562
+ const arrow = line.match(/^\s*=>\s*(\S.*)$/);
563
+ if (arrow) return arrow[1].trim();
564
+ if (line.trim() !== "") return void 0;
565
+ }
566
+ return void 0;
567
+ }
568
+ function scanCaseTags(text) {
569
+ const caseArgSpans = [];
570
+ const out = [];
571
+ CASE_NAME_REGEX.lastIndex = 0;
572
+ let m;
573
+ while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
574
+ const parenStart = m.index + m[0].length - 1;
575
+ const argsStr = extractBalancedParens(text, parenStart);
576
+ if (argsStr === null) continue;
577
+ const afterParen = parenStart + argsStr.length + 2;
578
+ if (caseArgSpans.some(([s, e]) => m.index >= s && m.index < e)) continue;
579
+ caseArgSpans.push([m.index, afterParen]);
580
+ const tagOffset = m.index + m[0].indexOf("@nudo:case");
581
+ const expectedText = extractCaseExpectedExpr(text, afterParen);
582
+ out.push({ name: m[1], argsText: argsStr, expectedText, tagOffset });
583
+ }
584
+ return out;
585
+ }
586
+ function scanCaseArgSpans(text) {
587
+ const spans = [];
588
+ CASE_NAME_REGEX.lastIndex = 0;
589
+ let m;
590
+ while ((m = CASE_NAME_REGEX.exec(text)) !== null) {
591
+ const parenStart = m.index + m[0].length - 1;
592
+ const argsStr = extractBalancedParens(text, parenStart);
593
+ if (argsStr === null) continue;
594
+ const afterParen = parenStart + argsStr.length + 2;
595
+ if (spans.some(([s, e]) => m.index >= s && m.index < e)) continue;
596
+ spans.push([m.index, afterParen]);
597
+ }
598
+ return spans;
599
+ }
600
+ function scanContractSegments(lines) {
601
+ const reqs = [];
602
+ for (const line of lines) {
603
+ const m = line.match(/^@nudo:contract\s+(.+)$/);
604
+ if (m) reqs.push(m[1].trim().replace(/\*\/$/, "").trim());
605
+ }
606
+ return reqs;
607
+ }
608
+ function scanThrowsDecl(lines) {
609
+ const kinds = /* @__PURE__ */ new Set();
610
+ let any2 = false;
611
+ for (const line of lines) {
612
+ const th = line.match(/^@nudo:throws\s+(.+)$/i);
613
+ if (th) {
614
+ const spec = th[1].trim();
615
+ if (spec === "*") any2 = true;
616
+ else {
617
+ for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
618
+ if (k === "*") any2 = true;
619
+ else kinds.add(k);
620
+ }
621
+ }
622
+ }
623
+ const cs = line.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
624
+ if (cs) {
625
+ const spec = (cs[1] ?? "*").trim();
626
+ if (spec === "" || spec === "*") any2 = true;
627
+ else {
628
+ for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
629
+ if (k === "*") any2 = true;
630
+ else kinds.add(k);
631
+ }
632
+ }
633
+ }
634
+ }
635
+ if (any2) return "*";
636
+ if (kinds.size > 0) return [...kinds].join(",");
637
+ return void 0;
638
+ }
639
+ function scanBudgetDecl(lines) {
640
+ const out = {};
641
+ for (const line of lines) {
642
+ const bd = line.match(/^@nudo:budget\s+(.+)$/i);
643
+ if (!bd) continue;
644
+ for (const part of bd[1].split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
645
+ const kv = part.match(/^(forks|calls|depth)\s*=\s*(\d+)$/i);
646
+ if (kv) {
647
+ const num = Number(kv[2]);
648
+ if (Number.isFinite(num) && num >= 1) {
649
+ out[kv[1].toLowerCase()] = Math.floor(num);
650
+ }
651
+ }
652
+ }
653
+ }
654
+ return Object.keys(out).length > 0 ? out : void 0;
655
+ }
49
656
 
50
657
  // src/algebra/hash-source.ts
51
658
  var lastHashSource;
@@ -73,6 +680,34 @@ function resetHashSourceCache() {
73
680
  function normPath(p) {
74
681
  return p.replace(/\\/g, "/");
75
682
  }
683
+ function stablePathKey(p) {
684
+ let fp = p;
685
+ if (fp.startsWith("file://")) {
686
+ fp = fp.slice("file://".length);
687
+ try {
688
+ fp = decodeURIComponent(fp);
689
+ } catch {
690
+ }
691
+ }
692
+ fp = normPath(fp);
693
+ const drive = /^\/([A-Za-z]:)(.*)$/.exec(fp);
694
+ if (drive) fp = `${drive[1]}${drive[2]}`;
695
+ if (/^[A-Za-z]:\//.test(fp)) return fp[0].toLowerCase() + fp.slice(1);
696
+ return fp;
697
+ }
698
+ function stablePathKeyGraph(m) {
699
+ const out = /* @__PURE__ */ new Map();
700
+ for (const [k, vs] of m) {
701
+ const key = stablePathKey(k);
702
+ let set = out.get(key);
703
+ if (!set) {
704
+ set = /* @__PURE__ */ new Set();
705
+ out.set(key, set);
706
+ }
707
+ for (const v2 of vs) set.add(stablePathKey(v2));
708
+ }
709
+ return out;
710
+ }
76
711
  function resolveDepPath(fromFile, spec) {
77
712
  const s = normPath(spec);
78
713
  if (!s.startsWith(".")) return s;
@@ -341,6 +976,28 @@ function omit(c, keys) {
341
976
  }
342
977
  return makeBuilder(c.prim, c.preds, { fields });
343
978
  }
979
+ var CONSTRAINT_BUILDERS = {
980
+ number,
981
+ string,
982
+ boolean,
983
+ any,
984
+ array,
985
+ shape,
986
+ lit: litC,
987
+ union,
988
+ nullable,
989
+ fn,
990
+ and: andC,
991
+ partial,
992
+ pick,
993
+ omit
994
+ };
995
+ var CONSTRAINT_BUILDER_NAMES = Object.keys(
996
+ CONSTRAINT_BUILDERS
997
+ );
998
+ var CONSTRAINT_EXPR_RE = new RegExp(
999
+ `^(${CONSTRAINT_BUILDER_NAMES.join("|")})\\s*\\(`
1000
+ );
344
1001
  function substTerm(t, paramName) {
345
1002
  if (t.op === "var" && t.id === SELF) return v(paramName);
346
1003
  if (t.op === "app" && t.fn === "get" && t.args.length === 2) {
@@ -593,15 +1250,21 @@ function throwConstraintToKinds(c) {
593
1250
  // src/algebra/refine.ts
594
1251
  function extractNudoImports(source) {
595
1252
  const out = [];
596
- const named = /@nudo:import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
597
- let m;
598
- while (m = named.exec(source)) {
599
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean).map((s) => s.split(/\s+as\s+/).pop().trim());
600
- out.push({ names, spec: m[2] });
1253
+ for (const rec of extractNudoImportRecords(source)) {
1254
+ out.push({ names: rec.names, spec: rec.spec });
601
1255
  }
602
- const ns = /@nudo:import\s+\*\s+as\s+(\w+)\s+from\s*["']([^"']+)["']/g;
603
- while (m = ns.exec(source)) {
604
- out.push({ names: [`*${m[1]}`], spec: m[2] });
1256
+ for (const bad of scanMalformedNudoImports(source)) {
1257
+ if (bad === "malformed-default") {
1258
+ collectDiag({
1259
+ code: "nudo:contract-syntax",
1260
+ message: `Default @nudo:import is not supported (use named { a, b } or namespace * as ns)`
1261
+ });
1262
+ } else {
1263
+ collectDiag({
1264
+ code: "nudo:contract-syntax",
1265
+ message: `Unrecognized @nudo:import form (expected { names } from "path" or * as ns from "path")`
1266
+ });
1267
+ }
605
1268
  }
606
1269
  return out;
607
1270
  }
@@ -643,21 +1306,7 @@ var NudoSidecarError = class extends Error {
643
1306
  this.code = code;
644
1307
  }
645
1308
  };
646
- var sidecarInjects = {
647
- number,
648
- string,
649
- boolean,
650
- shape,
651
- array,
652
- lit: litC,
653
- union,
654
- fn,
655
- and: andC,
656
- partial,
657
- pick,
658
- omit,
659
- nullable
660
- };
1309
+ var sidecarInjects = CONSTRAINT_BUILDERS;
661
1310
  function isSidecarSpec(spec) {
662
1311
  return spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts");
663
1312
  }
@@ -1024,7 +1673,12 @@ function sidecarDepClosure(src, fromFile, loadModule, out) {
1024
1673
  if (!isSidecarSpec(spec)) continue;
1025
1674
  const canonical = resolveDepPath(fromFile, spec);
1026
1675
  if (out.has(canonical)) continue;
1027
- const depSrc = loadModule(spec, fromFile);
1676
+ let depSrc;
1677
+ try {
1678
+ depSrc = loadModule(spec, fromFile);
1679
+ } catch {
1680
+ depSrc = void 0;
1681
+ }
1028
1682
  out.set(canonical, depSrc);
1029
1683
  if (depSrc === void 0) continue;
1030
1684
  const r = sidecarDepClosure(depSrc, canonical, loadModule, out);
@@ -1094,7 +1748,17 @@ function collectConstraints(source, opts) {
1094
1748
  });
1095
1749
  continue;
1096
1750
  }
1097
- const src = opts.loadModule(imp.spec, opts.fromFile);
1751
+ let src;
1752
+ try {
1753
+ src = opts.loadModule(imp.spec, opts.fromFile);
1754
+ } catch (e) {
1755
+ collectDiag({
1756
+ code: "nudo:interface-load",
1757
+ message: `sidecar '${imp.spec}' failed to load from '${opts.fromFile}': ${e instanceof Error ? e.message : String(e)}`,
1758
+ file: opts.fromFile
1759
+ });
1760
+ continue;
1761
+ }
1098
1762
  if (src === void 0) {
1099
1763
  collectDiag({
1100
1764
  code: "nudo:interface-load",
@@ -1150,26 +1814,8 @@ function collectConstraints(source, opts) {
1150
1814
  return map;
1151
1815
  }
1152
1816
  function extractRefineLines(source, fnName) {
1153
- const escaped = identBoundaryRegex(fnName);
1154
- const fnRe = new RegExp(
1155
- `(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}|const\\s+${escaped}\\s*=)`
1156
- );
1157
- const m = source.match(fnRe);
1158
- if (!m || m.index === void 0) return [];
1159
- const before = source.slice(0, m.index);
1160
- const lines = before.split("\n");
1161
- const reqs = [];
1162
- for (let i = lines.length - 1; i >= 0; i--) {
1163
- const line = lines[i].trim();
1164
- if (line === "" || line === "*/") continue;
1165
- if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
1166
- const rm = line.match(/@nudo:contract\s+(.+)$/);
1167
- if (rm) reqs.unshift(rm[1].trim().replace(/\*\/$/, "").trim());
1168
- continue;
1169
- }
1170
- break;
1171
- }
1172
- return reqs;
1817
+ const lines = fnDirectiveCommentLines(source, fnName);
1818
+ return scanContractSegments(lines);
1173
1819
  }
1174
1820
  function extractRefinesFromSource(source, fnName, opts = {}) {
1175
1821
  if (!stripStringsKeepComments(source).includes("@nudo:contract")) return [];
@@ -1179,7 +1825,14 @@ function extractRefinesFromSource(source, fnName, opts = {}) {
1179
1825
  const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
1180
1826
  for (const part of parts) {
1181
1827
  const m = part.match(/^(\w+)\s+([\w.]+)$/);
1182
- if (!m) continue;
1828
+ if (!m) {
1829
+ if (/^return\b/.test(part)) continue;
1830
+ collectDiag({
1831
+ code: "nudo:contract-syntax",
1832
+ message: `Malformed @nudo:contract segment '${part}' (expected <param> <constraintName> or return <constraintName>; e.g. @nudo:contract x positive)`
1833
+ });
1834
+ continue;
1835
+ }
1183
1836
  const [, param, cName] = m;
1184
1837
  if (param === "return") continue;
1185
1838
  const c = constraints.get(cName);
@@ -1209,7 +1862,15 @@ function extractRefineReturnFromSource(source, fnName, opts = {}) {
1209
1862
  const parts = line.split(/&&|,/).map((s) => s.trim()).filter(Boolean);
1210
1863
  for (const part of parts) {
1211
1864
  const m = part.match(/^return\s+([\w.]+)$/);
1212
- if (!m) continue;
1865
+ if (!m) {
1866
+ if (/^return\b/.test(part) && !/^return\s+[\w.]+$/.test(part)) {
1867
+ collectDiag({
1868
+ code: "nudo:contract-syntax",
1869
+ message: `Malformed @nudo:contract return segment '${part}' (expected return <constraintName>)`
1870
+ });
1871
+ }
1872
+ continue;
1873
+ }
1213
1874
  const cName = m[1];
1214
1875
  const c = constraints.get(cName);
1215
1876
  if (!c) continue;
@@ -1223,98 +1884,17 @@ function extractDeclaredThrows(source, fnName) {
1223
1884
  if (!directiveSrc.includes("@nudo:throws") && !directiveSrc.includes("!! throws")) {
1224
1885
  return void 0;
1225
1886
  }
1226
- const kinds = /* @__PURE__ */ new Set();
1227
- let any2 = false;
1228
- const escaped = identBoundaryRegex(fnName);
1229
- const fnRe = new RegExp(
1230
- `(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}|const\\s+${escaped}\\s*=)`
1231
- );
1232
- const m = source.match(fnRe);
1233
- if (!m || m.index === void 0) return void 0;
1234
- const before = source.slice(0, m.index);
1235
- const lines = before.split("\n");
1236
- let seenComment = false;
1237
- for (let i = lines.length - 1; i >= 0; i--) {
1238
- const line = lines[i].trim();
1239
- if (line === "") {
1240
- if (seenComment) break;
1241
- continue;
1242
- }
1243
- if (line === "*/") continue;
1244
- if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
1245
- seenComment = true;
1246
- const body = line.replace(/^[*/\s]+/, "").replace(/\*\/$/, "").trim();
1247
- const th = body.match(/@nudo:throws\s+(.+)$/i);
1248
- if (th) {
1249
- const spec = th[1].trim();
1250
- if (spec === "*") any2 = true;
1251
- else {
1252
- for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
1253
- if (k === "*") any2 = true;
1254
- else kinds.add(k);
1255
- }
1256
- }
1257
- }
1258
- const cs = body.match(/!!\s*throws(?:\s+([A-Za-z*][\w*|,\s]*))?/i);
1259
- if (cs) {
1260
- const spec = (cs[1] ?? "*").trim();
1261
- if (spec === "" || spec === "*") any2 = true;
1262
- else {
1263
- for (const k of spec.split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
1264
- if (k === "*") any2 = true;
1265
- else kinds.add(k);
1266
- }
1267
- }
1268
- }
1269
- continue;
1270
- }
1271
- break;
1272
- }
1273
- if (any2) return "*";
1274
- if (kinds.size > 0) return [...kinds];
1275
- return void 0;
1887
+ const lines = fnDirectiveCommentLines(source, fnName);
1888
+ const spec = scanThrowsDecl(lines);
1889
+ if (spec === void 0) return void 0;
1890
+ if (spec === "*") return "*";
1891
+ return spec.split(",").filter(Boolean);
1276
1892
  }
1277
1893
  function extractFnBudget(source, fnName) {
1278
1894
  const directiveSrc = stripStringsKeepComments(source);
1279
1895
  if (!directiveSrc.includes("@nudo:budget")) return void 0;
1280
- const escaped = identBoundaryRegex(fnName);
1281
- const fnRe = new RegExp(
1282
- `(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?(?:function\\s+${escaped}|const\\s+${escaped}\\s*=)`
1283
- );
1284
- const m = source.match(fnRe);
1285
- if (!m || m.index === void 0) return void 0;
1286
- const before = source.slice(0, m.index);
1287
- const lines = before.split("\n");
1288
- let seenComment = false;
1289
- const out = {};
1290
- for (let i = lines.length - 1; i >= 0; i--) {
1291
- const line = lines[i].trim();
1292
- if (line === "") {
1293
- if (seenComment) break;
1294
- continue;
1295
- }
1296
- if (line === "*/") continue;
1297
- if (line.startsWith("*") || line.startsWith("/*") || line.startsWith("//")) {
1298
- seenComment = true;
1299
- const body = line.replace(/^[*/\s]+/, "").replace(/\*\/$/, "").trim();
1300
- const bd = body.match(/@nudo:budget\s+(.+)$/i);
1301
- if (bd) {
1302
- for (const part of bd[1].split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean)) {
1303
- const kv = part.match(/^(forks|calls|depth)\s*=\s*(\d+)$/i);
1304
- if (kv) {
1305
- const n = Number(kv[2]);
1306
- if (Number.isFinite(n) && n >= 1) {
1307
- const key = kv[1].toLowerCase();
1308
- out[key] = Math.floor(n);
1309
- }
1310
- }
1311
- }
1312
- }
1313
- continue;
1314
- }
1315
- break;
1316
- }
1317
- return Object.keys(out).length > 0 ? out : void 0;
1896
+ const lines = fnDirectiveCommentLines(source, fnName);
1897
+ return scanBudgetDecl(lines);
1318
1898
  }
1319
1899
 
1320
1900
  // src/algebra/interface.ts
@@ -1631,7 +2211,20 @@ function loadSidecarBinding(source, fnName, opts) {
1631
2211
  if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return { ok: false };
1632
2212
  if (!localNamedExports(source).has(fnName)) return { ok: false };
1633
2213
  const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
1634
- const sidecarSrc = loadModule(spec, fromFile);
2214
+ let sidecarSrc;
2215
+ try {
2216
+ sidecarSrc = loadModule(spec, fromFile);
2217
+ } catch (e) {
2218
+ const reason = e instanceof Error ? e.message : String(e);
2219
+ if (noteSidecarLoadFailure(sidecarPath, reason)) {
2220
+ collectDiag2({
2221
+ code: "nudo:interface-load",
2222
+ message: `sidecar '${spec}' failed to load: ${reason}`,
2223
+ file: sidecarPath
2224
+ });
2225
+ }
2226
+ return { ok: false };
2227
+ }
1635
2228
  if (sidecarSrc === void 0) return { ok: false };
1636
2229
  if (sidecarSrc === source) return { ok: false };
1637
2230
  const scNames = sidecarExportNames(sidecarSrc);
@@ -1916,7 +2509,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
1916
2509
  const sidecarPath = sidecarPathOf(fromFile);
1917
2510
  if (!sidecarAutoBindAllowed(sidecarPath, autoBind, projectDir)) return void 0;
1918
2511
  const spec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
1919
- const sidecarSrc = loadModule(spec, fromFile);
2512
+ let sidecarSrc;
2513
+ try {
2514
+ sidecarSrc = loadModule(spec, fromFile);
2515
+ } catch {
2516
+ return void 0;
2517
+ }
1920
2518
  if (sidecarSrc === void 0) return void 0;
1921
2519
  const parts = [];
1922
2520
  const seen = /* @__PURE__ */ new Set([sidecarPath]);
@@ -1930,7 +2528,12 @@ function sidecarClosureFingerprint(fromFile, opts) {
1930
2528
  }
1931
2529
  const { spec: s, from } = queue.shift();
1932
2530
  const depPath = resolveDepPath(from, s);
1933
- const depSrc = loadModule(s, from);
2531
+ let depSrc;
2532
+ try {
2533
+ depSrc = loadModule(s, from);
2534
+ } catch {
2535
+ depSrc = void 0;
2536
+ }
1934
2537
  parts.push(`${depPath}=${depSrc === void 0 ? "miss" : hashSource(depSrc)}`);
1935
2538
  if (depSrc !== void 0) enqueueSidecarDeps(depSrc, depPath, seen, queue);
1936
2539
  }
@@ -2229,17 +2832,12 @@ function extractAllLoadSpecs(source) {
2229
2832
  const specs = /* @__PURE__ */ new Set();
2230
2833
  for (const imp of extractNudoImports(source)) specs.add(imp.spec);
2231
2834
  const isPathLikeSpec = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(spec);
2232
- const envRe = /@nudo:env\s+([^\n*]+)/g;
2233
- let envM;
2234
- while (envM = envRe.exec(source)) {
2235
- for (const part of envM[1].split(",")) {
2236
- const spec = part.trim().replace(/^['"]|['"]$/g, "");
2237
- if (spec && isPathLikeSpec(spec)) specs.add(spec);
2238
- }
2835
+ for (const name of extractFileEnvNames(source)) {
2836
+ if (isPathLikeSpec(name)) specs.add(name);
2837
+ }
2838
+ for (const rec of extractMockModuleRecords(source)) {
2839
+ if (rec.fromPath) specs.add(rec.fromPath);
2239
2840
  }
2240
- const mockFromRe = /@nudo:mock-module\s+"[^"]+"\s+(?:\{[^}]*\}\s+)?from\s+["']([^"']+)["']/g;
2241
- let mockM;
2242
- while (mockM = mockFromRe.exec(source)) specs.add(mockM[1]);
2243
2841
  const patterns = [
2244
2842
  /\bfrom\s*['"]([^'"]+)['"]/g,
2245
2843
  /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
@@ -2256,6 +2854,13 @@ function sidecarSpecsOf(source) {
2256
2854
  (spec) => (spec.startsWith(".") || spec.startsWith("/")) && (spec.endsWith(".nudo.js") || spec.endsWith(".nudo.ts"))
2257
2855
  );
2258
2856
  }
2857
+ function loadOrMiss(loadModule, spec, from) {
2858
+ try {
2859
+ return loadModule(spec, from);
2860
+ } catch {
2861
+ return void 0;
2862
+ }
2863
+ }
2259
2864
  function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2260
2865
  const parts = [];
2261
2866
  const paths = [];
@@ -2274,7 +2879,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2274
2879
  const sidecarPath = sidecarPathOf(entryFile);
2275
2880
  if (seen.has(sidecarPath)) return;
2276
2881
  const sidecarSpec = `./${sidecarPath.slice(sidecarPath.lastIndexOf("/") + 1)}`;
2277
- const sidecarSrc = loadModule(sidecarSpec, entryFile);
2882
+ const sidecarSrc = loadOrMiss(loadModule, sidecarSpec, entryFile);
2278
2883
  if (sidecarSrc === void 0) return;
2279
2884
  if (n >= MAX_LOAD_DEP_NODES) {
2280
2885
  truncated = true;
@@ -2298,7 +2903,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2298
2903
  if (seen.has(path)) continue;
2299
2904
  seen.add(path);
2300
2905
  n++;
2301
- const src = loadModule(cur.spec, cur.from);
2906
+ const src = loadOrMiss(loadModule, cur.spec, cur.from);
2302
2907
  parts.push(`sidecar:${path}=${src === void 0 ? "miss" : hashSource(src)}`);
2303
2908
  paths.push(path);
2304
2909
  contents.push({ path, content: src ?? null });
@@ -2319,7 +2924,7 @@ function loadModuleDepsFingerprint(source, loadModule, fromFile) {
2319
2924
  if (seen.has(path)) continue;
2320
2925
  seen.add(path);
2321
2926
  n++;
2322
- const src = loadModule(cur.spec, cur.from);
2927
+ const src = loadOrMiss(loadModule, cur.spec, cur.from);
2323
2928
  parts.push(`${path}=${src === void 0 ? "miss" : hashSource(src)}`);
2324
2929
  paths.push(path);
2325
2930
  contents.push({ path, content: src ?? null });
@@ -2929,7 +3534,7 @@ function unindexMemoKey(key) {
2929
3534
  function evictGeneralizeMemoForPaths(paths) {
2930
3535
  let n = 0;
2931
3536
  for (const raw of paths) {
2932
- const p = normPath(raw);
3537
+ const p = stablePathKey(raw);
2933
3538
  const keys = memoDepIndex.get(p);
2934
3539
  if (!keys) continue;
2935
3540
  for (const key of [...keys]) {
@@ -2971,7 +3576,7 @@ function generalizeMemoKey(fnName, source, opts) {
2971
3576
  const budget = opts.budget ?? defaultLeakBudget;
2972
3577
  const deps = opts.depsFp ?? (r ? refineDepsFingerprint(source, r) : { fp: "-", paths: [], contents: [], truncated: false });
2973
3578
  const sc = opts.sidecarFp ?? (r?.loadModule && r.fromFile ? sidecarClosureFingerprint(r.fromFile, r) : void 0);
2974
- const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ? normPath(sidecarPathOf(r.fromFile)) : void 0;
3579
+ const scPath = sc !== void 0 && !sc.startsWith("trunc:") && r?.fromFile ? sidecarPathOf(r.fromFile) : void 0;
2975
3580
  const srcPart = generalizeSourceKeyPart(source, fnName, opts.file);
2976
3581
  const key = [
2977
3582
  srcPart,
@@ -3007,8 +3612,9 @@ function generalizeMemoSet(key, value, depPaths) {
3007
3612
  }
3008
3613
  generalizeMemo.set(key, value);
3009
3614
  if (depPaths.length > 0) {
3010
- memoKeyDeps.set(key, depPaths);
3011
- for (const p of depPaths) {
3615
+ const normDeps = [...new Set(depPaths.map(stablePathKey))];
3616
+ memoKeyDeps.set(key, normDeps);
3617
+ for (const p of normDeps) {
3012
3618
  let set = memoDepIndex.get(p);
3013
3619
  if (!set) {
3014
3620
  set = /* @__PURE__ */ new Set();
@@ -3172,6 +3778,23 @@ function extractFn(source, fnName, fileAst) {
3172
3778
  formalsByName.set(exp, formalsByName.get(localName) ?? []);
3173
3779
  }
3174
3780
  }
3781
+ for (const scope of listFnDirectiveScopes(file)) {
3782
+ if (env.fns.has(scope.name)) continue;
3783
+ if (scope.name === "<anonymous>") continue;
3784
+ const n = scope.node;
3785
+ const isFnLike = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" || n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod";
3786
+ if (!isFnLike || !n.body) continue;
3787
+ if ((n.type === "ClassMethod" || n.type === "ClassPrivateMethod" || n.type === "ObjectMethod") && n.kind && n.kind !== "method") {
3788
+ continue;
3789
+ }
3790
+ const formals = formalParamsFromNodes(n.params ?? []);
3791
+ env.fns.set(scope.name, {
3792
+ params: formalParamDisplayNames(formals),
3793
+ body: n.body,
3794
+ async: n.async === true
3795
+ });
3796
+ formalsByName.set(scope.name, formals);
3797
+ }
3175
3798
  const fn2 = env.fns.get(fnName);
3176
3799
  if (!fn2) return void 0;
3177
3800
  return { params: fn2.params, body: fn2.body, env, formals: formalsByName.get(fnName) ?? [] };
@@ -3553,6 +4176,8 @@ function primMatches(prim, lv) {
3553
4176
  return typeof lv === "boolean";
3554
4177
  case "object":
3555
4178
  return lv === null;
4179
+ case "undefined":
4180
+ return lv === void 0;
3556
4181
  default:
3557
4182
  return false;
3558
4183
  }
@@ -3560,8 +4185,9 @@ function primMatches(prim, lv) {
3560
4185
  function predHolds(lv, p) {
3561
4186
  switch (p.op) {
3562
4187
  case "eq": {
3563
- const v2 = selfEqLiteral(p);
3564
- return v2 !== void 0 && lv === v2;
4188
+ const litT = isSelfVar(p.a) && p.b.op === "lit" ? p.b : isSelfVar(p.b) && p.a.op === "lit" ? p.a : void 0;
4189
+ if (!litT) return false;
4190
+ return Object.is(lv, litT.value);
3565
4191
  }
3566
4192
  case "gt":
3567
4193
  case "ge":
@@ -3579,11 +4205,6 @@ function predHolds(lv, p) {
3579
4205
  return false;
3580
4206
  }
3581
4207
  }
3582
- function selfEqLiteral(p) {
3583
- if (isSelfVar(p.a) && p.b.op === "lit") return p.b.value;
3584
- if (isSelfVar(p.b) && p.a.op === "lit") return p.a.value;
3585
- return void 0;
3586
- }
3587
4208
  function cmpHolds(x, op, n) {
3588
4209
  switch (op) {
3589
4210
  case "gt":
@@ -3872,7 +4493,8 @@ function extractLiteralEvidence(rec, idx) {
3872
4493
  const absArg = rec.argAbs?.[idx];
3873
4494
  if (!absArg) return void 0;
3874
4495
  if (absArg.conf !== "exact" && absArg.conf !== "path") return void 0;
3875
- const lv = litValue(absArg);
4496
+ const lvR = litValue(absArg);
4497
+ const lv = lvR.ok ? lvR.value : void 0;
3876
4498
  if (typeof lv === "number" || typeof lv === "string" || typeof lv === "boolean") {
3877
4499
  return lv;
3878
4500
  }
@@ -4390,7 +5012,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4390
5012
  }
4391
5013
  arg = projected;
4392
5014
  }
4393
- const lv = litValue(arg);
5015
+ const lvR = litValue(arg);
5016
+ const lv = lvR.ok ? lvR.value : void 0;
4394
5017
  const isStr = arg.shape.k === "prim" && arg.shape.type === "string";
4395
5018
  const strLen = typeof lv === "string" ? lv.length : void 0;
4396
5019
  const argIsUndef = isExactUndef(arg);
@@ -4599,7 +5222,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4599
5222
  }
4600
5223
  }
4601
5224
  if (isIntFlag(constraint)) {
4602
- const iv = litValue(fieldAbs);
5225
+ const ivR = litValue(fieldAbs);
5226
+ const iv = ivR.ok ? ivR.value : void 0;
4603
5227
  if (typeof iv === "number" && !Number.isInteger(iv)) {
4604
5228
  out.push({
4605
5229
  severity: "error",
@@ -4641,7 +5265,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4641
5265
  });
4642
5266
  }
4643
5267
  }
4644
- const lv = litValue(fieldAbs);
5268
+ const lvR = litValue(fieldAbs);
5269
+ const lv = lvR.ok ? lvR.value : void 0;
4645
5270
  const sv = typeof lv === "string" ? lv.length : void 0;
4646
5271
  const isStr = fieldAbs.shape.k === "prim" && fieldAbs.shape.type === "string";
4647
5272
  for (const p of constraint.preds) {
@@ -4694,7 +5319,9 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4694
5319
  }
4695
5320
  }
4696
5321
  }
4697
- if (lv !== void 0 && (typeof lv === "number" || typeof lv === "string" || typeof lv === "boolean") && ((constraint.members?.length ?? 0) > 0 || constraint.preds.some((p) => p.op === "eq")) && !literalMeetsConstraint(lv, constraint)) {
5322
+ const fieldLit = fieldAbs.term?.op === "lit" ? fieldAbs.term.value : void 0;
5323
+ const fieldIsLit = fieldAbs.term?.op === "lit";
5324
+ if (fieldIsLit && (typeof fieldLit === "number" || typeof fieldLit === "string" || typeof fieldLit === "boolean" || fieldLit === null || fieldLit === void 0) && ((constraint.members?.length ?? 0) > 0 || constraint.preds.some((p) => p.op === "eq")) && !literalMeetsConstraint(fieldLit, constraint)) {
4698
5325
  out.push({
4699
5326
  severity: "error",
4700
5327
  code: "nudo:constraint-violated",
@@ -4778,7 +5405,8 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
4778
5405
  }
4779
5406
  }
4780
5407
  if (isIntFlag(c)) {
4781
- const iv = litValue(arg);
5408
+ const ivR = litValue(arg);
5409
+ const iv = ivR.ok ? ivR.value : void 0;
4782
5410
  if (typeof iv === "number" && !Number.isInteger(iv)) {
4783
5411
  out.push({
4784
5412
  severity: "error",
@@ -5092,9 +5720,26 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
5092
5720
  }
5093
5721
 
5094
5722
  export {
5723
+ listFnDirectiveScopes,
5724
+ findFnDirectiveScope,
5725
+ fnDirectiveCommentLines,
5726
+ parseEnvPayload,
5727
+ parseMockModulePayload,
5728
+ parseNudoImportPayload,
5729
+ extractFileEnvNames,
5730
+ extractMockModuleRecords,
5731
+ extractNudoImportRecords,
5732
+ extractBalancedParens,
5733
+ scanCaseTags,
5734
+ scanCaseArgSpans,
5735
+ scanContractSegments,
5736
+ scanThrowsDecl,
5737
+ scanBudgetDecl,
5095
5738
  hashSource,
5096
5739
  resetHashSourceCache,
5097
5740
  normPath,
5741
+ stablePathKey,
5742
+ stablePathKeyGraph,
5098
5743
  resolveDepPath,
5099
5744
  isNodeModulesPath,
5100
5745
  sidecarPathOf,
@@ -5118,6 +5763,9 @@ export {
5118
5763
  partial,
5119
5764
  pick,
5120
5765
  omit,
5766
+ CONSTRAINT_BUILDERS,
5767
+ CONSTRAINT_BUILDER_NAMES,
5768
+ CONSTRAINT_EXPR_RE,
5121
5769
  instantiateConstraint,
5122
5770
  instantiateOnTerm,
5123
5771
  constraintToEntryAbs,