lemmascript 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.
package/README.md CHANGED
@@ -1,18 +1,24 @@
1
1
  # LemmaScript (Tech Preview)
2
2
 
3
- A verification toolchain for TypeScript. Write ordinary TypeScript with `//@ ` specification annotations. The toolchain generates verified code from your TypeScript — either Lean 4 (with Velvet/Loom) or Dafny.
3
+ A verification toolchain for TypeScript. Write ordinary TypeScript with `//@ ` specification annotations. The toolchain generates verifiable code from your TypeScript — either in Dafny or Lean 4 (with Velvet/Loom).
4
4
 
5
- See [SPEC.md](SPEC.md) for the full specification, [DESIGN.md](DESIGN.md) for the Lean backend design, and [DESIGN_DAFNY.md](DESIGN_DAFNY.md) for the Dafny backend design.
5
+ See [SPEC.md](SPEC.md) and [DESIGN.md](DESIGN.md).
6
6
 
7
7
  This is a **Tech Preview**: the core idea is there, but support, semantics, and ergonomics are still evolving.
8
8
 
9
- ## Case Studies
9
+ ## Examples and Case Studies
10
10
 
11
- Each case study is verified in both Lean 4 and Dafny from the same annotated TypeScript source.
11
+ Each example and case study is verified in Lean 4 and/or Dafny from the same annotated TypeScript source.
12
12
 
13
+ See the internal [examples](examples).
14
+
15
+ See the external case studies:
13
16
  - **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
14
17
  - **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
15
18
  - **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
19
+ - **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. [Cookie verification done **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79). Dafny only.
20
+ - **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination; 5 helper lemmas, 28 loop invariants). Dafny only.
21
+ - **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (replace — bounded length), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Dafny only.
16
22
 
17
23
  ## Setup
18
24
 
@@ -40,13 +46,6 @@ git clone https://github.com/namin/velvet.git -b lemma ../velvet
40
46
 
41
47
  ## Usage
42
48
 
43
- ### Lean backend (default)
44
-
45
- ```sh
46
- npx lsc gen src/myModule.ts
47
- lake build
48
- ```
49
-
50
49
  ### Dafny backend
51
50
 
52
51
  ```sh
@@ -57,6 +56,13 @@ npx lsc regen --backend=dafny src/myModule.ts
57
56
 
58
57
  The Dafny backend generates two files per TS source: `foo.dfy.gen` (always regeneratable) and `foo.dfy` (source of truth, with LLM/user proof additions). The diff between them must be additions-only.
59
58
 
59
+ ### Lean backend
60
+
61
+ ```sh
62
+ npx lsc gen src/myModule.ts
63
+ lake build
64
+ ```
65
+
60
66
  ## What's Supported
61
67
 
62
68
  ### Annotations
@@ -71,33 +77,21 @@ The Dafny backend generates two files per TS source: `foo.dfy.gen` (always regen
71
77
 
72
78
  ## File Structure
73
79
 
74
- ### Lean backend
80
+ ### Dafny backend
75
81
 
76
82
  | File | Generated? | Purpose |
77
83
  |------|-----------|---------|
78
84
  | `foo.ts` | — | TypeScript source with `//@ ` annotations |
79
- | `foo.types.lean` | Yes | Lean types, `namespace Pure` defs |
80
- | `foo.spec.lean` | No | Ghost definitions, helper lemmas |
81
- | `foo.def.lean` | Yes | Velvet method definitions |
82
- | `foo.proof.lean` | No | `prove_correct` with proof tactics |
85
+ | `foo.dfy.gen` | Yes | Generated Dafny (merge base, always regeneratable) |
86
+ | `foo.dfy` | Yes (initial) | Annotated Dafny (gen + proof additions) |
83
87
 
84
- ### Dafny backend
88
+ ### Lean backend
85
89
 
86
90
  | File | Generated? | Purpose |
87
91
  |------|-----------|---------|
88
92
  | `foo.ts` | — | TypeScript source with `//@ ` annotations |
89
- | `foo.dfy.gen` | Yes | Generated Dafny (merge base, always regeneratable) |
90
- | `foo.dfy` | Yes (initial) | Annotated Dafny (gen + proof additions) |
93
+ | `foo.types.lean` | Yes | Lean types, `namespace Pure` defs |
94
+ | `foo.spec.lean` | No | Ghost definitions, helper lemmas |
95
+ | `foo.def.lean` | Yes | Velvet method definitions |
96
+ | `foo.proof.lean` | No | `prove_correct` with proof tactics |
91
97
 
92
- ## Examples
93
-
94
- | Example | Pattern |
95
- |---------|---------|
96
- | `binarySearch` | Array search, break, Int arithmetic |
97
- | `linearSearch` | Loop with break, Nat index |
98
- | `arraySum` | Accumulator, recursive ghost function |
99
- | `transition` | State machine, enum ADT, inter-method call |
100
- | `packet` | Discriminated union with data, if-chain → match |
101
- | `isSorted` | Loop with break, existential in invariant |
102
- | `maxElement` | If-without-else in loop |
103
- | `arrayContains` | For-of loop, boolean flag |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dafny backend commands: gen, check, regen.
3
3
  */
4
- import { existsSync, readFileSync, writeFileSync, copyFileSync } from "fs";
4
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from "fs";
5
5
  import { execSync } from "child_process";
6
6
  import path from "path";
7
7
  function writeGen(genPath, text) {
@@ -31,74 +31,68 @@ export function dafnyCheckDiff(genPath, dfyPath) {
31
31
  catch { /* diff not available */ }
32
32
  return true;
33
33
  }
34
- export function dafnyVerify(dfyPath, dir) {
34
+ export function dafnyVerify(dfyPath, dir, timeLimit) {
35
35
  console.log("Running dafny verify...");
36
36
  try {
37
37
  const content = readFileSync(dfyPath, "utf-8");
38
- const stdLibFlag = content.includes("import Std.") ? " --standard-libraries" : "";
39
- execSync(`dafny verify${stdLibFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
38
+ const stdLibFlag = content.includes("Std.") ? " --standard-libraries" : "";
39
+ const timeLimitFlag = timeLimit ? ` --verification-time-limit ${timeLimit}` : "";
40
+ execSync(`dafny verify${stdLibFlag}${timeLimitFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
40
41
  return true;
41
42
  }
42
43
  catch {
43
44
  return false;
44
45
  }
45
46
  }
46
- export function dafnySavePatch(genPath, dfyPath, patchPath) {
47
- try {
48
- const patch = execSync(`diff -u "${genPath}" "${dfyPath}" || true`, { encoding: "utf-8" });
49
- writeFileSync(patchPath, patch);
50
- console.log(`Saved: ${patchPath}`);
51
- }
52
- catch { /* diff not available */ }
53
- }
54
- function dafnyApplyPatch(genPath, dfyPath, patchPath) {
55
- if (!existsSync(patchPath))
56
- return false;
57
- const patch = readFileSync(patchPath, "utf-8").trim();
58
- if (!patch)
59
- return false;
60
- copyFileSync(genPath, dfyPath);
61
- try {
62
- execSync(`patch --no-backup-if-mismatch -p0 "${dfyPath}" < "${patchPath}"`, { stdio: "pipe" });
63
- console.log(`Applied: ${patchPath}`);
64
- return true;
65
- }
66
- catch {
67
- copyFileSync(genPath, dfyPath);
68
- return false;
69
- }
70
- }
71
- export function dafnyRegen(genPath, dfyPath, patchPath, text, dir) {
72
- // 1. No .dfy yet — create both, verify, done
47
+ export function dafnyRegen(genPath, dfyPath, basePath, text, dir) {
48
+ // 1. Read old gen before overwriting (needed for base seeding)
49
+ const oldGen = existsSync(genPath) ? readFileSync(genPath, "utf-8") : "";
50
+ // 2. Always write new gen so user can inspect latest output
51
+ writeGen(genPath, text);
52
+ // 3. No .dfy yet — create dfy, verify, done
73
53
  if (!existsSync(dfyPath)) {
74
- dafnyGen(genPath, dfyPath, text);
54
+ writeFileSync(dfyPath, text);
55
+ console.log(`Created: ${path.basename(dfyPath)}`);
75
56
  if (!dafnyVerify(dfyPath, dir)) {
76
57
  console.error(`FAILED: ${path.basename(dfyPath)} verification failed on first run.`);
77
58
  process.exit(1);
78
59
  }
79
60
  return;
80
61
  }
81
- // 2. Capture patch from current gen dfy BEFORE overwriting gen
82
- let hasPatch = false;
83
- if (existsSync(genPath)) {
84
- dafnySavePatch(genPath, dfyPath, patchPath);
85
- hasPatch = readFileSync(patchPath, "utf-8").trim().length > 0;
62
+ // 4. Determine anchor: base file if it exists (dirty state), otherwise old gen
63
+ const anchor = existsSync(basePath) ? readFileSync(basePath, "utf-8") : oldGen;
64
+ // 5. If gen changed, three-way merge
65
+ if (text !== anchor) {
66
+ const savedDfy = readFileSync(dfyPath, "utf-8");
67
+ if (!existsSync(basePath))
68
+ writeFileSync(basePath, anchor);
69
+ const mergedPath = dfyPath + ".merged";
70
+ console.log("Gen changed. Three-way merging...");
71
+ try {
72
+ execSync(`git merge-file "${dfyPath}" "${basePath}" "${genPath}"`, { stdio: "pipe" });
73
+ console.log(`Merged: ${path.basename(dfyPath)}`);
74
+ }
75
+ catch (e) {
76
+ if (e.status > 0) {
77
+ copyFileSync(dfyPath, mergedPath);
78
+ writeFileSync(dfyPath, savedDfy);
79
+ console.error(`CONFLICT: ${path.basename(dfyPath)} — merge had conflicts, dfy restored. See ${path.basename(mergedPath)}`);
80
+ process.exit(1);
81
+ }
82
+ throw e;
83
+ }
86
84
  }
87
- // 3. Write new gen
88
- writeGen(genPath, text);
89
- // 4. Try verifying existing dfy as-is
90
- if (dafnyVerify(dfyPath, dir))
91
- return;
92
- // 5. Failed — try applying captured patch to new gen
93
- if (hasPatch) {
94
- console.log("Verification failed. Trying to apply patch...");
95
- if (dafnyApplyPatch(genPath, dfyPath, patchPath) && dafnyVerify(dfyPath, dir))
96
- return;
85
+ // 6. Check gen invariant (unconditional)
86
+ if (!dafnyCheckDiff(genPath, dfyPath)) {
87
+ console.error(`FAILED: ${path.basename(dfyPath)} has modifications to generated lines.`);
88
+ process.exit(1);
89
+ }
90
+ // 7. Verify
91
+ if (!dafnyVerify(dfyPath, dir)) {
92
+ console.error(`FAILED: ${path.basename(dfyPath)} verification failed.`);
93
+ process.exit(1);
97
94
  }
98
- // 6. Needs LLM re-adaptation
99
- console.error(`FAILED: ${path.basename(dfyPath)} needs manual re-adaptation.`);
100
- console.error(` ${genPath} has the new generated code.`);
101
- if (hasPatch)
102
- console.error(` ${patchPath} has the captured patch.`);
103
- process.exit(1);
95
+ // 8. Success delete base (gen is now the anchor)
96
+ if (existsSync(basePath))
97
+ unlinkSync(basePath);
104
98
  }