@nudojs/core 0.1.0 → 0.3.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +26 -0
  4. package/dist/index.d.ts +1633 -11
  5. package/dist/index.js +9965 -107
  6. package/package.json +28 -2
  7. package/src/__tests__/fn-sig-impl.test.ts +57 -0
  8. package/src/__tests__/ops-refined.test.ts +196 -0
  9. package/src/__tests__/ops.test.ts +8 -33
  10. package/src/__tests__/type-value-refined.test.ts +210 -0
  11. package/src/__tests__/type-value.test.ts +106 -0
  12. package/src/algebra/__tests__/abs-native.test.ts +176 -0
  13. package/src/algebra/__tests__/arithmetic.test.ts +170 -0
  14. package/src/algebra/__tests__/ast-eval.test.ts +154 -0
  15. package/src/algebra/__tests__/bridge.test.ts +160 -0
  16. package/src/algebra/__tests__/call-budget.test.ts +117 -0
  17. package/src/algebra/__tests__/check-case-consistency.test.ts +69 -0
  18. package/src/algebra/__tests__/check-gold.test.ts +290 -0
  19. package/src/algebra/__tests__/check-json.test.ts +57 -0
  20. package/src/algebra/__tests__/check-mini-repo.test.ts +49 -0
  21. package/src/algebra/__tests__/check-real-commander.test.ts +43 -0
  22. package/src/algebra/__tests__/check-real-packages.test.ts +165 -0
  23. package/src/algebra/__tests__/check-recall-gold.test.ts +1024 -0
  24. package/src/algebra/__tests__/check-require-dep-memo.test.ts +45 -0
  25. package/src/algebra/__tests__/check-return-contract.test.ts +90 -0
  26. package/src/algebra/__tests__/check-shape-gold.test.ts +303 -0
  27. package/src/algebra/__tests__/check-source-memo.test.ts +204 -0
  28. package/src/algebra/__tests__/check.test.ts +69 -0
  29. package/src/algebra/__tests__/denote.test.ts +77 -0
  30. package/src/algebra/__tests__/diagnostics.test.ts +64 -0
  31. package/src/algebra/__tests__/differential.test.ts +124 -0
  32. package/src/algebra/__tests__/div-mod.test.ts +38 -0
  33. package/src/algebra/__tests__/example0.test.ts +103 -0
  34. package/src/algebra/__tests__/exec-analyze-throws.test.ts +60 -0
  35. package/src/algebra/__tests__/exec-async.test.ts +46 -0
  36. package/src/algebra/__tests__/exec-bpath.test.ts +193 -0
  37. package/src/algebra/__tests__/exec-class.test.ts +56 -0
  38. package/src/algebra/__tests__/exec-e2e.test.ts +97 -0
  39. package/src/algebra/__tests__/exec-generator.test.ts +53 -0
  40. package/src/algebra/__tests__/exec-inherit-default.test.ts +111 -0
  41. package/src/algebra/__tests__/exec-optional-nested.test.ts +87 -0
  42. package/src/algebra/__tests__/exec-rest.test.ts +55 -0
  43. package/src/algebra/__tests__/exec-spread-forof.test.ts +85 -0
  44. package/src/algebra/__tests__/exec-static-computed.test.ts +61 -0
  45. package/src/algebra/__tests__/exec-switch.test.ts +74 -0
  46. package/src/algebra/__tests__/exec-trycatch.test.ts +71 -0
  47. package/src/algebra/__tests__/fn-fp-memo.test.ts +166 -0
  48. package/src/algebra/__tests__/fn-fp-multidecl.test.ts +49 -0
  49. package/src/algebra/__tests__/format.test.ts +34 -0
  50. package/src/algebra/__tests__/generalize-memo.test.ts +285 -0
  51. package/src/algebra/__tests__/generalize.test.ts +113 -0
  52. package/src/algebra/__tests__/hash-source.test.ts +19 -0
  53. package/src/algebra/__tests__/hof.test.ts +80 -0
  54. package/src/algebra/__tests__/inlay.test.ts +30 -0
  55. package/src/algebra/__tests__/language.test.ts +77 -0
  56. package/src/algebra/__tests__/leq.test.ts +109 -0
  57. package/src/algebra/__tests__/load-deps-fp-memo.test.ts +132 -0
  58. package/src/algebra/__tests__/methods.test.ts +37 -0
  59. package/src/algebra/__tests__/node-types.test.ts +26 -0
  60. package/src/algebra/__tests__/nudo-constraints.ts +46 -0
  61. package/src/algebra/__tests__/objects.test.ts +117 -0
  62. package/src/algebra/__tests__/parse-source-cache.test.ts +42 -0
  63. package/src/algebra/__tests__/refine-import.test.ts +109 -0
  64. package/src/algebra/__tests__/stable-source-key-block.test.ts +23 -0
  65. package/src/algebra/__tests__/stable-source-key.test.ts +31 -0
  66. package/src/algebra/__tests__/surface.test.ts +41 -0
  67. package/src/algebra/__tests__/this-inherit.test.ts +130 -0
  68. package/src/algebra/abs-fn.ts +49 -0
  69. package/src/algebra/abs-modules.ts +139 -0
  70. package/src/algebra/abs.ts +206 -0
  71. package/src/algebra/arithmetic.ts +691 -0
  72. package/src/algebra/ast-eval.ts +1763 -0
  73. package/src/algebra/bridge.ts +350 -0
  74. package/src/algebra/builtins.ts +373 -0
  75. package/src/algebra/check-report.ts +150 -0
  76. package/src/algebra/check.ts +1867 -0
  77. package/src/algebra/constraint.ts +304 -0
  78. package/src/algebra/denote.ts +166 -0
  79. package/src/algebra/diagnostics.ts +210 -0
  80. package/src/algebra/exec/call.ts +29 -0
  81. package/src/algebra/exec/calls.ts +96 -0
  82. package/src/algebra/exec/class-registry.ts +29 -0
  83. package/src/algebra/exec/class.ts +329 -0
  84. package/src/algebra/exec/index.ts +6 -0
  85. package/src/algebra/exec/member-diag.ts +141 -0
  86. package/src/algebra/exec/run.ts +341 -0
  87. package/src/algebra/exec/runtime.ts +583 -0
  88. package/src/algebra/exec/transpile.ts +993 -0
  89. package/src/algebra/fn-fp.ts +294 -0
  90. package/src/algebra/format.ts +91 -0
  91. package/src/algebra/generalize.ts +776 -0
  92. package/src/algebra/hash-source.ts +27 -0
  93. package/src/algebra/index.ts +34 -0
  94. package/src/algebra/inlay.ts +198 -0
  95. package/src/algebra/language.ts +289 -0
  96. package/src/algebra/leak.ts +68 -0
  97. package/src/algebra/leq.ts +301 -0
  98. package/src/algebra/load-deps-fp.ts +97 -0
  99. package/src/algebra/methods.ts +128 -0
  100. package/src/algebra/objects.ts +251 -0
  101. package/src/algebra/parse-source.ts +64 -0
  102. package/src/algebra/phi.ts +42 -0
  103. package/src/algebra/pred.ts +395 -0
  104. package/src/algebra/refine.ts +227 -0
  105. package/src/algebra/stable-source-key.ts +81 -0
  106. package/src/algebra/surface.ts +206 -0
  107. package/src/algebra/template.ts +143 -0
  108. package/src/algebra/term.ts +119 -0
  109. package/src/environment.ts +5 -0
  110. package/src/index.ts +52 -1
  111. package/src/mock-helpers.ts +166 -0
  112. package/src/ops.ts +106 -62
  113. package/src/refinements/range.ts +86 -0
  114. package/src/refinements/template-predicates.ts +11 -0
  115. package/src/refinements/template.ts +176 -0
  116. package/src/strip-types.ts +236 -0
  117. package/src/type-value.ts +193 -12
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ absToTypeValue,
4
+ typeValueToAbs,
5
+ bridgeIsLossy,
6
+ getTvConfidence,
7
+ numLit,
8
+ numVar,
9
+ gtNum,
10
+ v,
11
+ litValue,
12
+ formatShape,
13
+ analyzeFn,
14
+ objOf,
15
+ strLit,
16
+ boolLit,
17
+ } from "../index.ts";
18
+ import { typeValueToString, T } from "../../type-value.ts";
19
+
20
+ describe("bridge Abs → TypeValue", () => {
21
+ it("literal 4 → literal 4 #exact", () => {
22
+ const tv = absToTypeValue(numLit(4));
23
+ expect(tv.kind).toBe("literal");
24
+ if (tv.kind === "literal") expect(tv.value).toBe(4);
25
+ expect(getTvConfidence(tv)).toBe("exact");
26
+ });
27
+
28
+ it("plain number prim → primitive number", () => {
29
+ const tv = absToTypeValue({
30
+ shape: { k: "prim", type: "number" },
31
+ conf: "exact",
32
+ });
33
+ expect(typeValueToString(tv)).toBe("number");
34
+ });
35
+
36
+ it("x>0 with pred → refined number", () => {
37
+ const a = numVar("x", gtNum(v("x"), 0));
38
+ const tv = absToTypeValue(a);
39
+ // term 是 var,会丢 term;pred 可 encode 为 refined
40
+ expect(tv.kind === "refined" || tv.kind === "primitive").toBe(true);
41
+ const lossy = bridgeIsLossy(a);
42
+ // 丢 term 是预期的
43
+ expect(lossy.reasons.some((r) => r.includes("term"))).toBe(true);
44
+ });
45
+
46
+ it("object slots map to object TypeValue", () => {
47
+ const o = objOf({
48
+ host: { value: strLit("localhost") },
49
+ port: { value: numLit(8080) },
50
+ debug: { value: boolLit(false) },
51
+ });
52
+ const tv = absToTypeValue(o);
53
+ expect(tv.kind).toBe("object");
54
+ expect(typeValueToString(tv)).toContain("host");
55
+ expect(typeValueToString(tv)).toContain("8080");
56
+ });
57
+ });
58
+
59
+ describe("bridge TypeValue → Abs", () => {
60
+ it("literal 6 → Abs exact 6", () => {
61
+ const abs = typeValueToAbs(T.literal(6));
62
+ expect(litValue(abs)).toBe(6);
63
+ expect(abs.conf).toBe("exact");
64
+ });
65
+
66
+ it("primitive string → prim string", () => {
67
+ const abs = typeValueToAbs(T.string);
68
+ expect(formatShape(abs)).toBe("string");
69
+ });
70
+
71
+ it("object → obj slots", () => {
72
+ const abs = typeValueToAbs(
73
+ T.object({ a: T.literal(1), b: T.string }),
74
+ );
75
+ expect(abs.shape.k).toBe("obj");
76
+ });
77
+
78
+ it("round-trip literal", () => {
79
+ const tv = absToTypeValue(numLit(42));
80
+ const back = typeValueToAbs(tv);
81
+ expect(litValue(back)).toBe(42);
82
+ });
83
+
84
+ it("round-trip primitive (lossless)", () => {
85
+ const abs0 = {
86
+ shape: { k: "prim" as const, type: "number" as const },
87
+ conf: "exact" as const,
88
+ };
89
+ const tv = absToTypeValue(abs0);
90
+ const back = typeValueToAbs(tv);
91
+ expect(formatShape(back)).toBe("number");
92
+ });
93
+ });
94
+
95
+ describe("bridgeIsLossy", () => {
96
+ it("literal is not lossy", () => {
97
+ expect(bridgeIsLossy(numLit(1)).lossy).toBe(false);
98
+ });
99
+ it("symbolic term is lossy", () => {
100
+ const src = `
101
+ function scale(x) { return x + 1; }
102
+ `;
103
+ const r = analyzeFn(src, "scale", [numVar("x")]);
104
+ expect(bridgeIsLossy(r).lossy).toBe(true);
105
+ });
106
+ });
107
+
108
+ describe("bridge any / conf / refined reverse (rewrite closeout)", () => {
109
+ it("Abs any projects as unknown but keeps path conf (not partial)", () => {
110
+ const a: import("../abs.ts").Abs = { shape: { k: "any" }, conf: "path" };
111
+ const tv = absToTypeValue(a);
112
+ expect(tv.kind).toBe("unknown");
113
+ expect(getTvConfidence(tv)).toBe("path");
114
+ });
115
+
116
+ it("Abs analysis-fail unknown keeps partial conf", () => {
117
+ const a: import("../abs.ts").Abs = { shape: { k: "unknown" }, conf: "partial" };
118
+ const tv = absToTypeValue(a);
119
+ expect(tv.kind).toBe("unknown");
120
+ expect(getTvConfidence(tv)).toBe("partial");
121
+ });
122
+
123
+ it("widened TypeValue does not round-trip as exact", () => {
124
+ const src = `
125
+ function scale(x) { return x + 1; }
126
+ `;
127
+ const r = analyzeFn(src, "scale", [numVar("x")]);
128
+ const tv = absToTypeValue(r);
129
+ const back = typeValueToAbs(tv);
130
+ expect(back.conf === "exact").toBe(false);
131
+ });
132
+
133
+ it("refined numeric pred reverse-encodes into Abs pred", () => {
134
+ const a = numVar("x", gtNum(v("x"), 0));
135
+ const tv = absToTypeValue(a);
136
+ expect(tv.kind).toBe("refined");
137
+ const back = typeValueToAbs(tv);
138
+ expect(back.shape.k).toBe("prim");
139
+ expect(back.pred).toBeDefined();
140
+ expect(back.pred!.op).toBe("gt");
141
+ // pred 右端恢复为字面量 0
142
+ if (back.pred!.op === "gt") {
143
+ expect(back.pred!.b.op).toBe("lit");
144
+ if (back.pred!.b.op === "lit") expect(back.pred!.b.value).toBe(0);
145
+ }
146
+ });
147
+
148
+ it("refined le reverse-encodes", () => {
149
+ const a: import("../abs.ts").Abs = {
150
+ shape: { k: "prim", type: "number" },
151
+ term: v("n"),
152
+ pred: { op: "le", a: v("n"), b: { op: "lit", value: 10 } },
153
+ conf: "path",
154
+ };
155
+ const tv = absToTypeValue(a);
156
+ expect(tv.kind).toBe("refined");
157
+ const back = typeValueToAbs(tv);
158
+ expect(back.pred?.op).toBe("le");
159
+ });
160
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { checkSource } from "../check.ts";
3
+ import {
4
+ analyzeFn,
5
+ evalProgramAbs,
6
+ resetAbsCallBudget,
7
+ MAX_CALL_DEPTH,
8
+ } from "../ast-eval.ts";
9
+ import { pTrue } from "../pred.ts";
10
+ import { num, unknown } from "../abs.ts";
11
+ import type { Abs } from "../abs.ts";
12
+
13
+ const FAC = `
14
+ function fac(n) {
15
+ if (n <= 1) return 1;
16
+ return n * fac(n - 1);
17
+ }
18
+ `;
19
+
20
+ const FIB = `
21
+ function fib(n) {
22
+ if (n <= 1) return n;
23
+ return fib(n - 1) + fib(n - 2);
24
+ }
25
+ `;
26
+
27
+ const MUTUAL = `
28
+ function isEven(n) {
29
+ if (n === 0) return true;
30
+ return isOdd(n - 1);
31
+ }
32
+ function isOdd(n) {
33
+ if (n === 0) return false;
34
+ return isEven(n - 1);
35
+ }
36
+ `;
37
+
38
+ describe("Abs call budget (recursion guard)", () => {
39
+ it("analyzeFn on factorial does not stack-overflow", () => {
40
+ resetAbsCallBudget();
41
+ const r = analyzeFn(FAC, "fac", [num()], pTrue);
42
+ expect(r).toBeDefined();
43
+ expect(r.shape.k === "unknown" || r.shape.k === "prim" || r.shape.k === "never").toBe(true);
44
+ });
45
+
46
+ it("analyzeFn on fib does not stack-overflow", () => {
47
+ resetAbsCallBudget();
48
+ const r = analyzeFn(FIB, "fib", [num()], pTrue);
49
+ expect(r).toBeDefined();
50
+ });
51
+
52
+ it("mutual recursion does not stack-overflow", () => {
53
+ resetAbsCallBudget();
54
+ const r = analyzeFn(MUTUAL, "isEven", [num()], pTrue);
55
+ expect(r).toBeDefined();
56
+ });
57
+
58
+ it("evalProgramAbs with top-level recursive call does not stack-overflow", () => {
59
+ resetAbsCallBudget();
60
+ const { last } = evalProgramAbs(`
61
+ function count(n) {
62
+ if (n <= 0) return 0;
63
+ return 1 + count(n - 1);
64
+ }
65
+ const x = count(5);
66
+ `);
67
+ expect(last).toBeDefined();
68
+ });
69
+
70
+ it("concrete base-case recursion still evaluates", () => {
71
+ // 调用深度在 64 内:count(5) 应给出字面量结果而非 opaque
72
+ resetAbsCallBudget();
73
+ const lit5: Abs = {
74
+ shape: { k: "prim", type: "number" },
75
+ term: { op: "lit", value: 5 },
76
+ conf: "exact",
77
+ };
78
+ const r = analyzeFn(
79
+ `
80
+ function count(n) {
81
+ if (n <= 0) return 0;
82
+ return 1 + count(n - 1);
83
+ }
84
+ `,
85
+ "count",
86
+ [lit5],
87
+ pTrue,
88
+ );
89
+ // 5 层展开应收敛为字面量 5(或至少 number)
90
+ expect(r.shape.k === "prim" || r.shape.k === "never").toBe(true);
91
+ if (r.term && r.term.op === "lit") {
92
+ expect(r.term.value).toBe(5);
93
+ }
94
+ });
95
+
96
+ it("checkSource completes on factorial with structured diagnostics", () => {
97
+ const report = checkSource("/tmp/fac.js", FAC, pTrue, {});
98
+ expect(report).toBeDefined();
99
+ // 不得崩溃;递归截断记 warning
100
+ const codes = report.issues.map((i) => i.code);
101
+ expect(codes).toContain("nudo:recursion-truncated");
102
+ expect(report.issues.every((i) => i.severity !== undefined)).toBe(true);
103
+ });
104
+
105
+ it("depth budget constant is aligned with TypeValue evaluator", () => {
106
+ expect(MAX_CALL_DEPTH).toBe(64);
107
+ });
108
+
109
+ it("truncated result uses opaque conf, not partial-any", () => {
110
+ resetAbsCallBudget();
111
+ const r = analyzeFn(FAC, "fac", [num()], pTrue);
112
+ // 截断后应为 unknown+opaque,或仍能算出的 prim;绝不能是 exact 的假结果
113
+ if (r.shape.k === "unknown") {
114
+ expect(r.conf === "opaque" || r.conf === "partial" || r.conf === "widened").toBe(true);
115
+ }
116
+ });
117
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * case ⊄ refine → nudo:case-inconsistency
3
+ * case 是契约的见证,不是另一套前置来源。
4
+ */
5
+ import { describe, it, expect } from "vitest";
6
+ import { checkSource, pTrue } from "../index.ts";
7
+ import { withStdImport, stdOpts } from "./nudo-constraints.ts";
8
+
9
+ function issuesOf(src: string) {
10
+ return checkSource("/t/case.js", withStdImport(src), pTrue, stdOpts);
11
+ }
12
+
13
+ describe("case vs refine", () => {
14
+ it("ok: case 实参满足契约", () => {
15
+ const r = issuesOf(`
16
+ /**
17
+ * @nudo:refine x positive
18
+ * @nudo:case "ok" (5)
19
+ */
20
+ function needsPositive(x) {
21
+ return x;
22
+ }
23
+ `);
24
+ expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
25
+ });
26
+
27
+ it("error: case 实参 ⊭ refine", () => {
28
+ const r = issuesOf(`
29
+ /**
30
+ * @nudo:refine x positive
31
+ * @nudo:case "neg" (-1)
32
+ */
33
+ function needsPositive(x) {
34
+ return x;
35
+ }
36
+ `);
37
+ const err = r.issues.find((i) => i.code === "nudo:case-inconsistency");
38
+ expect(err).toBeDefined();
39
+ expect(err!.severity).toBe("error");
40
+ expect(err!.expected).toContain(">");
41
+ expect(err!.message).toContain("neg");
42
+ });
43
+
44
+ it("error: percent 上界违例", () => {
45
+ const r = issuesOf(`
46
+ /**
47
+ * @nudo:refine n percent
48
+ * @nudo:case "big" (150)
49
+ */
50
+ function pct(n) {
51
+ return n;
52
+ }
53
+ `);
54
+ expect(r.ok).toBe(false);
55
+ expect(r.issues.some((i) => i.code === "nudo:case-inconsistency")).toBe(true);
56
+ });
57
+
58
+ it("ok: 无 refine 的 case 不报", () => {
59
+ const r = issuesOf(`
60
+ /**
61
+ * @nudo:case "any" (-1)
62
+ */
63
+ function id(x) {
64
+ return x;
65
+ }
66
+ `);
67
+ expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
68
+ });
69
+ });
@@ -0,0 +1,290 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { checkSource, pTrue } from "../index.ts";
3
+ import { withStdImport, stdOpts } from "./nudo-constraints.ts";
4
+
5
+ /**
6
+ * nudo check 金标:多形态 JS 片段上的门禁行为。
7
+ * 每条:source + 期望 ok / 期望出现的 issue code。
8
+ */
9
+
10
+ type Gold = {
11
+ name: string;
12
+ source: string;
13
+ expectOk: boolean;
14
+ expectCode?: string;
15
+ };
16
+
17
+ const golds: Gold[] = [
18
+ {
19
+ name: "valid positive call",
20
+ source: `
21
+ /**
22
+ * @nudo:refine x positive
23
+ */
24
+ function needsPositive(x) {
25
+ if (x > 0) return x;
26
+ return 0;
27
+ }
28
+ const r = needsPositive(5);
29
+ `,
30
+ expectOk: true,
31
+ },
32
+ {
33
+ name: "negative call violates x>0",
34
+ source: `
35
+ /**
36
+ * @nudo:refine x positive
37
+ */
38
+ function needsPositive(x) {
39
+ if (x > 0) return x;
40
+ return 0;
41
+ }
42
+ const r = needsPositive(-1);
43
+ `,
44
+ expectOk: false,
45
+ expectCode: "nudo:constraint-violated",
46
+ },
47
+ {
48
+ name: "range guard lo is clamp not violation",
49
+ source: `
50
+ function clampId(id) {
51
+ if (id < 1) return 1;
52
+ if (id > 9999) return 9999;
53
+ return id;
54
+ }
55
+ clampId(0);
56
+ `,
57
+ // `id < 1` 是回退守卫,不作为调用前置
58
+ expectOk: true,
59
+ },
60
+ {
61
+ name: "range guard valid mid",
62
+ source: `
63
+ function clampId(id) {
64
+ if (id < 1) return 1;
65
+ if (id > 9999) return 9999;
66
+ return id;
67
+ }
68
+ clampId(50);
69
+ `,
70
+ expectOk: true,
71
+ },
72
+ {
73
+ name: "pure arithmetic no constraints",
74
+ source: `
75
+ function add(a, b) { return a + b; }
76
+ const r = add(1, 2);
77
+ `,
78
+ expectOk: true,
79
+ },
80
+ {
81
+ name: "ESM export function still listed",
82
+ source: `
83
+ export function scale(x) { return x + 1; }
84
+ `,
85
+ expectOk: true,
86
+ },
87
+ {
88
+ name: "async function listed",
89
+ source: `
90
+ export async function load(id) { return id + 1; }
91
+ `,
92
+ expectOk: true,
93
+ },
94
+ {
95
+ name: "class constructor listed",
96
+ source: `
97
+ class Counter {
98
+ constructor(n) { this.n = n; }
99
+ get() { return this.n; }
100
+ }
101
+ export function main() { return new Counter(1).get(); }
102
+ `,
103
+ expectOk: true,
104
+ },
105
+ // --- 真阳性:必须报 ---
106
+ {
107
+ name: "zero violates x>0",
108
+ source: `
109
+ /**
110
+ * @nudo:refine x positive
111
+ */
112
+ function needsPositive(x) {
113
+ if (x > 0) return x;
114
+ return 0;
115
+ }
116
+ needsPositive(0);
117
+ `,
118
+ expectOk: false,
119
+ expectCode: "nudo:constraint-violated",
120
+ },
121
+ {
122
+ name: "unary negative call",
123
+ source: `
124
+ /**
125
+ * @nudo:refine x positive
126
+ */
127
+ function needsPositive(x) {
128
+ if (x > 0) return x;
129
+ return 0;
130
+ }
131
+ needsPositive(-3);
132
+ `,
133
+ expectOk: false,
134
+ expectCode: "nudo:constraint-violated",
135
+ },
136
+ {
137
+ name: "x>=1 rejects 0",
138
+ source: `
139
+ /**
140
+ * @nudo:refine i atLeast1
141
+ */
142
+ function idx(i) {
143
+ if (i >= 1) return i;
144
+ return 1;
145
+ }
146
+ idx(0);
147
+ `,
148
+ expectOk: false,
149
+ expectCode: "nudo:constraint-violated",
150
+ },
151
+ {
152
+ name: "upper bound x<10 rejects 10",
153
+ source: `
154
+ /**
155
+ * @nudo:refine n small
156
+ */
157
+ function small(n) {
158
+ if (n < 10) return n;
159
+ return 9;
160
+ }
161
+ small(10);
162
+ `,
163
+ expectOk: false,
164
+ expectCode: "nudo:constraint-violated",
165
+ },
166
+ // --- 真阴性:不得误报(clamp / 无前置) ---
167
+ {
168
+ name: "clamp negative input is not violation",
169
+ source: `
170
+ function clamp(n, lo, hi) {
171
+ if (n < lo) return lo;
172
+ if (n > hi) return hi;
173
+ return n;
174
+ }
175
+ clamp(-5, 0, 100);
176
+ `,
177
+ expectOk: true,
178
+ },
179
+ {
180
+ name: "no guard no constraint",
181
+ source: `
182
+ function double(n) { return n * 2; }
183
+ double(-1);
184
+ `,
185
+ expectOk: true,
186
+ },
187
+ {
188
+ name: "equality guard is not numeric precondition",
189
+ source: `
190
+ function onlyZero(x) {
191
+ if (x === 0) return 0;
192
+ return 1;
193
+ }
194
+ onlyZero(5);
195
+ `,
196
+ expectOk: true,
197
+ },
198
+ {
199
+ name: "min boundary valid",
200
+ source: `
201
+ function needsPositive(x) {
202
+ if (x > 0) return x;
203
+ return 0;
204
+ }
205
+ needsPositive(1);
206
+ `,
207
+ expectOk: true,
208
+ },
209
+ // --- 结构 ---
210
+ {
211
+ name: "arrow function constraint",
212
+ source: `
213
+ /**
214
+ * @nudo:refine x positive
215
+ */
216
+ const needsPositive = (x) => {
217
+ if (x > 0) return x;
218
+ return 0;
219
+ };
220
+ needsPositive(-2);
221
+ `,
222
+ expectOk: false,
223
+ expectCode: "nudo:constraint-violated",
224
+ },
225
+ {
226
+ name: "export default function",
227
+ source: `
228
+ /**
229
+ * @nudo:refine x positive
230
+ */
231
+ export default function needsPositive(x) {
232
+ if (x > 0) return x;
233
+ return 0;
234
+ }
235
+ needsPositive(-1);
236
+ `,
237
+ expectOk: false,
238
+ expectCode: "nudo:constraint-violated",
239
+ },
240
+ {
241
+ name: "both bounds mid valid",
242
+ source: `
243
+ /**
244
+ * @nudo:refine n percent
245
+ */
246
+ function pct(n) {
247
+ if (n >= 0 && n <= 100) return n;
248
+ return 0;
249
+ }
250
+ pct(50);
251
+ `,
252
+ expectOk: true,
253
+ },
254
+ {
255
+ name: "both bounds high invalid",
256
+ source: `
257
+ /**
258
+ * @nudo:refine n percent
259
+ */
260
+ function pct(n) {
261
+ if (n >= 0 && n <= 100) return n;
262
+ return 0;
263
+ }
264
+ pct(150);
265
+ `,
266
+ expectOk: false,
267
+ expectCode: "nudo:constraint-violated",
268
+ },
269
+ ];
270
+
271
+ describe("nudo check gold standards", () => {
272
+ for (const g of golds) {
273
+ it(g.name, () => {
274
+ const report = checkSource("gold.js", withStdImport(g.source), pTrue, stdOpts);
275
+ expect(report.ok, formatIssues(report)).toBe(g.expectOk);
276
+ if (g.expectCode) {
277
+ expect(
278
+ report.issues.some((i) => i.code === g.expectCode),
279
+ formatIssues(report),
280
+ ).toBe(true);
281
+ }
282
+ // 每个 case 至少能列出函数或明确无函数
283
+ expect(Array.isArray(report.signatures)).toBe(true);
284
+ });
285
+ }
286
+ });
287
+
288
+ function formatIssues(r: { issues: Array<{ severity: string; code: string; message: string }> }): string {
289
+ return r.issues.map((i) => `${i.severity} ${i.code}: ${i.message}`).join("; ") || "(none)";
290
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { checkSource, serializeCheckJson, pTrue } from "../index.ts";
3
+ import { withStdImport, stdOpts } from "./nudo-constraints.ts";
4
+
5
+ describe("CheckJson contract v1", () => {
6
+ const src = `
7
+ /**
8
+ * @nudo:refine x positive
9
+ */
10
+ function needsPositive(x) {
11
+ if (x > 0) return x;
12
+ return 0;
13
+ }
14
+ needsPositive(-1);
15
+ `;
16
+
17
+ it("has stable top-level fields", () => {
18
+ const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
19
+ expect(j.version).toBe(1);
20
+ expect(j.file).toBe("c.js");
21
+ expect(j.ok).toBe(false);
22
+ expect(j.summary).toMatchObject({
23
+ errors: expect.any(Number),
24
+ warnings: expect.any(Number),
25
+ infos: expect.any(Number),
26
+ functions: expect.any(Number),
27
+ });
28
+ expect(Array.isArray(j.signatures)).toBe(true);
29
+ expect(Array.isArray(j.issues)).toBe(true);
30
+ });
31
+
32
+ it("signatures carry abs display + detail", () => {
33
+ const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
34
+ const sig = j.signatures.find((s) => s.name === "needsPositive");
35
+ expect(sig).toBeDefined();
36
+ expect(sig!.params).toEqual(["x"]);
37
+ expect(sig!.abs).toContain("#");
38
+ expect(typeof sig!.conf).toBe("string");
39
+ });
40
+
41
+ it("issues carry actual/expected for violations", () => {
42
+ const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
43
+ const err = j.issues.find((i) => i.code === "nudo:constraint-violated");
44
+ expect(err).toBeDefined();
45
+ expect(err!.actual).toBeDefined();
46
+ expect(err!.expected).toBeDefined();
47
+ expect(err!.line).toBeGreaterThan(0);
48
+ });
49
+
50
+ it("ok file has ok:true and empty error issues", () => {
51
+ const j = serializeCheckJson(
52
+ checkSource("ok.js", `function f(x){ return x+1; }\nf(1);\n`, pTrue, stdOpts),
53
+ );
54
+ expect(j.ok).toBe(true);
55
+ expect(j.issues.filter((i) => i.severity === "error")).toEqual([]);
56
+ });
57
+ });