lemmafit 0.0.1 → 0.2.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -4
  3. package/blank-template/README.md +3 -0
  4. package/blank-template/SPEC.yaml +1 -0
  5. package/blank-template/index.html +12 -0
  6. package/blank-template/lemmafit/.vibe/config.json +5 -0
  7. package/blank-template/lemmafit/dafny/Domain.dfy +5 -0
  8. package/blank-template/lemmafit/dafny/Replay.dfy +147 -0
  9. package/blank-template/package.json +25 -0
  10. package/blank-template/src/App.css +3 -0
  11. package/blank-template/src/App.tsx +10 -0
  12. package/blank-template/src/dafny/.gitkeep +0 -0
  13. package/blank-template/src/index.css +29 -0
  14. package/blank-template/src/main.tsx +10 -0
  15. package/blank-template/src/vite-env.d.ts +6 -0
  16. package/blank-template/template.gitignore +3 -0
  17. package/blank-template/tsconfig.json +21 -0
  18. package/blank-template/tsconfig.node.json +11 -0
  19. package/blank-template/vite.config.js +9 -0
  20. package/cli/context-hook.js +103 -0
  21. package/cli/daemon.js +24 -0
  22. package/cli/download-dafny2js.js +136 -0
  23. package/cli/generate-guarantees-md.js +223 -0
  24. package/cli/lemmafit.js +385 -0
  25. package/cli/session-hook.js +74 -0
  26. package/cli/sync.js +168 -0
  27. package/cli/verify-hook.js +221 -0
  28. package/commands/guarantees.md +138 -0
  29. package/docs/CLAUDE_INSTRUCTIONS.md +137 -0
  30. package/kernels/Replay.dfy +147 -0
  31. package/lib/daemon-client.js +54 -0
  32. package/lib/daemon.js +990 -0
  33. package/lib/download-dafny.js +130 -0
  34. package/lib/log.js +32 -0
  35. package/lib/spawn-claude.js +51 -0
  36. package/package.json +49 -5
  37. package/skills/lemmafit-dafny/SKILL.md +101 -0
  38. package/skills/lemmafit-post-react-audit/SKILL.md +46 -0
  39. package/skills/lemmafit-pre-react-audits/SKILL.md +67 -0
  40. package/skills/lemmafit-proofs/SKILL.md +24 -0
  41. package/skills/lemmafit-react-pattern/SKILL.md +62 -0
  42. package/skills/lemmafit-spec/SKILL.md +71 -0
  43. package/index.js +0 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 midspiral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,9 +1,98 @@
1
1
  # lemmafit
2
2
 
3
- Formal verification for web apps.
3
+ Make agents **prove** that their code is correct.
4
4
 
5
- This package is under active development and not yet functional. We will be releasing soon.
5
+ Read our launch post: [Introducing lemmafit: A Verifier in the AI Loop](https://midspiral.com/blog/introducing-lemmafit-a-verifier-in-the-ai-loop/).
6
6
 
7
- You can stay up to date at midspiral.com
7
+ Lemmafit integrates [Dafny](https://dafny.org/) formal verification into your development workflow via [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Business logic, state machines, and other logic are written in Dafny, mathematically verified, then auto-compiled to TypeScript for use in your React app.
8
8
 
9
- See the basis for the product on [GitHub](https://github.com/metareflection/dafny-replay).
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Install lemmafit globally
13
+ npm install -g lemmafit
14
+
15
+ # Create a new project
16
+ lemmafit init PROJECT_NAME
17
+ cd PROJECT_NAME
18
+
19
+ # Install deps (downloads Dafny automatically)
20
+ npm install
21
+
22
+ # In one terminal, start the verification daemon
23
+ npm run daemon
24
+
25
+ # In another terminal, start the Vite dev server
26
+ npm run dev
27
+
28
+ # In a third terminal, open Claude Code
29
+ claude
30
+ ```
31
+
32
+ ## Use Cases / Considerations
33
+
34
+ - lemmafit works with greenfield projects only. You must begin a project with lemmafit. Support for existing codebases is in the pipeline.
35
+
36
+ - lemmafit compiles Dafny to Typescript which then hooks into a React app. In the future, we will support other languages and frameworks.
37
+
38
+ - lemmafit is optimized to work with Claude Code. In the future, lemmafit will be agent-agnostic.
39
+
40
+ ## How It Works
41
+
42
+ 1. Prompt Claude Code as you normally would. You may use a simple starting prompt or a structured prompting system.
43
+ **Example: "Create a pomodoro app I can use personally and locally."**
44
+ 2. The agent will write a `SPEC.yaml` and write verified logic in `lemmafit/dafny/Domain.dfy`
45
+ 3. The **daemon** watches `.dfy` files, runs `dafny verify`, and on success compiles to `src/dafny/Domain.cjs` + `src/dafny/app.ts`
46
+ 4. The agent will hook the generated TypeScript API into a React app — the logic is proven correct
47
+ 5. After proofs complete, run custom command in Claude Code `/guarantees` to activate claimcheck and generate a guarantees report
48
+
49
+ ## Project Structure
50
+
51
+ ```
52
+ my-app/
53
+ ├── SPEC.yaml # Your requirements
54
+ ├── lemmafit/
55
+ │ ├── dafny/
56
+ │ │ └── Domain.dfy # Your verified Dafny logic
57
+ │ │ └── Replay.dfy # Generic Replay kernel
58
+ │ ├── .vibe/
59
+ │ │ ├── config.json # Project config
60
+ │ │ ├── status.json # Verification status (generated)
61
+ │ │ └── claims.json # Proof obligations (generated)
62
+ │ └── reports/
63
+ │ └── guarantees.md # Guarantee report (generated)
64
+ ├── src/
65
+ │ ├── dafny/
66
+ │ │ ├── Domain.cjs # Compiled JS (generated)
67
+ │ │ └── app.ts # TypeScript API (generated - DO NOT EDIT)
68
+ │ ├── App.tsx # Your React app
69
+ │ └── main.tsx
70
+ ├── .claude/ # Hooks & settings (managed by lemmafit)
71
+ └── package.json
72
+ ```
73
+
74
+ ## CLI
75
+
76
+ ```bash
77
+ lemmafit init [dir] # Create project from template
78
+ lemmafit sync [dir] # Re-sync system files (.claude/, hooks)
79
+ lemmafit daemon [dir] # Run verification daemon standalone
80
+ lemmafit logs [dir] # View daemon log
81
+ lemmafit logs --clear [dir] # Clear daemon log
82
+ ```
83
+
84
+ ## Updating
85
+
86
+ System files sync automatically on install:
87
+
88
+ ```bash
89
+ npm update lemmafit
90
+ # postinstall re-syncs .claude/settings.json, hooks, and instructions
91
+ ```
92
+
93
+ ## Requirements
94
+
95
+ - Node.js 18+
96
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI
97
+
98
+ Dafny and dafny2js are downloaded automatically during `npm install` to `~/.lemmafit/`.
@@ -0,0 +1,3 @@
1
+ # Verified App
2
+
3
+ A lemmafit project using Dafny and React.
@@ -0,0 +1 @@
1
+ entries: []
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Verified App</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ {
2
+ "entry": "lemmafit/dafny/Domain.dfy",
3
+ "appCore": "AppCore",
4
+ "outputName": "Domain"
5
+ }
@@ -0,0 +1,5 @@
1
+ // Verified domain logic
2
+ // This file is the source of truth for your app's state machine.
3
+ // The daemon watches this file and recompiles on changes.
4
+
5
+ include "Replay.dfy"
@@ -0,0 +1,147 @@
1
+ abstract module {:compile false} Domain {
2
+ type Model
3
+ type Action
4
+
5
+ ghost predicate Inv(m: Model)
6
+
7
+ function Init(): Model
8
+ function Apply(m: Model, a: Action): Model
9
+ requires Inv(m)
10
+ function Normalize(m: Model): Model
11
+
12
+ lemma InitSatisfiesInv()
13
+ ensures Inv(Init())
14
+
15
+ lemma StepPreservesInv(m: Model, a: Action)
16
+ requires Inv(m)
17
+ ensures Inv(Normalize(Apply(m,a)))
18
+ }
19
+
20
+ abstract module {:compile false} Kernel {
21
+ import D : Domain
22
+
23
+ function Step(m: D.Model, a: D.Action): D.Model
24
+ requires D.Inv(m)
25
+ {
26
+ D.Normalize(D.Apply(m, a))
27
+ }
28
+
29
+ function InitHistory(): History {
30
+ History([], D.Init(), [])
31
+ }
32
+
33
+ datatype History =
34
+ History(past: seq<D.Model>, present: D.Model, future: seq<D.Model>)
35
+
36
+ function Do(h: History, a: D.Action): History
37
+ requires D.Inv(h.present)
38
+ {
39
+ History(h.past + [h.present], Step(h.present, a), [])
40
+ }
41
+
42
+ // Apply action without recording to history (for live preview during drag)
43
+ function Preview(h: History, a: D.Action): History
44
+ requires D.Inv(h.present)
45
+ {
46
+ History(h.past, Step(h.present, a), h.future)
47
+ }
48
+
49
+ // Commit current state, recording baseline to history (for end of drag)
50
+ function CommitFrom(h: History, baseline: D.Model): History {
51
+ History(h.past + [baseline], h.present, [])
52
+ }
53
+
54
+ function Undo(h: History): History {
55
+ if |h.past| == 0 then h
56
+ else
57
+ var i := |h.past| - 1;
58
+ History(h.past[..i], h.past[i], [h.present] + h.future)
59
+ }
60
+
61
+ function Redo(h: History): History {
62
+ if |h.future| == 0 then h
63
+ else
64
+ History(h.past + [h.present], h.future[0], h.future[1..])
65
+ }
66
+
67
+ lemma DoPreservesInv(h: History, a: D.Action)
68
+ requires D.Inv(h.present)
69
+ ensures D.Inv(Do(h, a).present)
70
+ {
71
+ D.StepPreservesInv(h.present, a);
72
+ }
73
+
74
+ ghost predicate HistInv(h: History) {
75
+ (forall i | 0 <= i < |h.past| :: D.Inv(h.past[i])) &&
76
+ D.Inv(h.present) &&
77
+ (forall j | 0 <= j < |h.future| :: D.Inv(h.future[j]))
78
+ }
79
+
80
+ lemma InitHistorySatisfiesInv()
81
+ ensures HistInv(InitHistory())
82
+ {
83
+ D.InitSatisfiesInv();
84
+ }
85
+
86
+ lemma UndoPreservesHistInv(h: History)
87
+ requires HistInv(h)
88
+ ensures HistInv(Undo(h))
89
+ {
90
+ }
91
+
92
+ lemma RedoPreservesHistInv(h: History)
93
+ requires HistInv(h)
94
+ ensures HistInv(Redo(h))
95
+ {
96
+ }
97
+
98
+ lemma DoPreservesHistInv(h: History, a: D.Action)
99
+ requires HistInv(h)
100
+ ensures HistInv(Do(h, a))
101
+ {
102
+ D.StepPreservesInv(h.present, a);
103
+ }
104
+
105
+ lemma PreviewPreservesHistInv(h: History, a: D.Action)
106
+ requires HistInv(h)
107
+ ensures HistInv(Preview(h, a))
108
+ {
109
+ D.StepPreservesInv(h.present, a);
110
+ }
111
+
112
+ lemma CommitFromPreservesHistInv(h: History, baseline: D.Model)
113
+ requires HistInv(h)
114
+ requires D.Inv(baseline)
115
+ ensures HistInv(CommitFrom(h, baseline))
116
+ {
117
+ }
118
+
119
+ // proxy for linear undo: after a new action, there is no redo branch
120
+ lemma DoHasNoRedoBranch(h: History, a: D.Action)
121
+ requires HistInv(h)
122
+ ensures Redo(Do(h, a)) == Do(h, a)
123
+ {
124
+ }
125
+ // round-tripping properties
126
+ lemma UndoRedoRoundTrip(h: History)
127
+ requires |h.past| > 0
128
+ ensures Redo(Undo(h)) == h
129
+ {
130
+ }
131
+ lemma RedoUndoRoundTrip(h: History)
132
+ requires |h.future| > 0
133
+ ensures Undo(Redo(h)) == h
134
+ {
135
+ }
136
+ // idempotence at boundaries
137
+ lemma UndoAtBeginningIsNoOp(h: History)
138
+ requires |h.past| == 0
139
+ ensures Undo(h) == h
140
+ {
141
+ }
142
+ lemma RedoAtEndIsNoOp(h: History)
143
+ requires |h.future| == 0
144
+ ensures Redo(h) == h
145
+ {
146
+ }
147
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "verified-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "daemon": "lemmafit-daemon",
9
+ "build": "tsc && vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lemmafit": "^0.1.0",
14
+ "bignumber.js": "^9.1.2",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.0",
20
+ "@types/react-dom": "^18.2.0",
21
+ "@vitejs/plugin-react": "^4.2.0",
22
+ "typescript": "^5.3.0",
23
+ "vite": "^5.0.0"
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ .app {
2
+ padding: 2rem;
3
+ }
@@ -0,0 +1,10 @@
1
+ import './App.css'
2
+
3
+ function App() {
4
+ return (
5
+ <div className="app">
6
+ </div>
7
+ )
8
+ }
9
+
10
+ export default App
File without changes
@@ -0,0 +1,29 @@
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ display: flex;
19
+ place-items: center;
20
+ min-width: 320px;
21
+ min-height: 100vh;
22
+ }
23
+
24
+ #root {
25
+ max-width: 1280px;
26
+ margin: 0 auto;
27
+ padding: 2rem;
28
+ text-align: center;
29
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.cjs?raw' {
4
+ const content: string;
5
+ export default content;
6
+ }
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ *~
3
+ lemmafit/.vibe/daemon.sock
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true
9
+ },
10
+ "include": ["vite.config.js"]
11
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ optimizeDeps: {
7
+ include: ['bignumber.js'],
8
+ },
9
+ })
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code UserPromptSubmit hook for lemmafit.
4
+ *
5
+ * Reads lemmafit/.vibe/status.json and writes it to stdout so Claude
6
+ * sees the current verification status before processing every prompt.
7
+ *
8
+ * Hook receives JSON on stdin with { "cwd": "..." }
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const { initLog, log } = require('../lib/log');
14
+
15
+ async function readStdin() {
16
+ const chunks = [];
17
+ for await (const chunk of process.stdin) {
18
+ chunks.push(chunk);
19
+ }
20
+ return Buffer.concat(chunks).toString('utf8');
21
+ }
22
+
23
+ function findProjectRoot(dir) {
24
+ let current = dir;
25
+ while (current !== path.dirname(current)) {
26
+ if (fs.existsSync(path.join(current, 'lemmafit'))) {
27
+ return current;
28
+ }
29
+ current = path.dirname(current);
30
+ }
31
+ return null;
32
+ }
33
+
34
+ async function main() {
35
+ const input = await readStdin();
36
+
37
+ let hookData;
38
+ try {
39
+ hookData = JSON.parse(input);
40
+ } catch {
41
+ process.exit(0);
42
+ }
43
+
44
+ const cwd = hookData.cwd;
45
+ if (!cwd) {
46
+ process.exit(0);
47
+ }
48
+
49
+ const projectDir = findProjectRoot(cwd);
50
+ if (!projectDir) {
51
+ process.exit(0);
52
+ }
53
+
54
+ initLog(projectDir);
55
+ log('context', 'Injecting status into prompt');
56
+
57
+ const statusPath = path.join(projectDir, 'lemmafit', '.vibe', 'status.json');
58
+ let status;
59
+ try {
60
+ status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
61
+ } catch {
62
+ log('context', 'status.json missing or unreadable');
63
+ const context = `<lemmafit-status>
64
+ { "state": "unavailable", "error": "status.json missing or unreadable — is the daemon running?" }
65
+ </lemmafit-status>`;
66
+ console.log(JSON.stringify({
67
+ hookSpecificOutput: {
68
+ hookEventName: 'UserPromptSubmit',
69
+ additionalContext: context,
70
+ }
71
+ }));
72
+ process.exit(0);
73
+ }
74
+
75
+ // Inject status with full spec queue items so Claude can act on them directly
76
+ const summary = {
77
+ state: status.state,
78
+ compiled: status.compiled,
79
+ lastCompiled: status.lastCompiled,
80
+ timestamp: status.timestamp,
81
+ files: status.files,
82
+ axioms: status.axioms,
83
+ compileError: status.compileError || undefined,
84
+ specQueue: status.specQueue || [],
85
+ };
86
+
87
+ const context = `<lemmafit-status>
88
+ ${JSON.stringify(summary, null, 2)}
89
+ </lemmafit-status>
90
+ <lemmafit-status-file>${statusPath}</lemmafit-status-file>`;
91
+
92
+ console.log(JSON.stringify({
93
+ hookSpecificOutput: {
94
+ hookEventName: 'UserPromptSubmit',
95
+ additionalContext: context,
96
+ }
97
+ }));
98
+ }
99
+
100
+ main().catch((err) => {
101
+ console.error('Context hook error:', err.message);
102
+ process.exit(1);
103
+ });
package/cli/daemon.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lemmafit daemon CLI - watches and verifies Dafny files.
4
+ *
5
+ * Usage:
6
+ * lemmafit-daemon [project-dir] [--once]
7
+ */
8
+
9
+ const path = require('path');
10
+ const { Daemon } = require('../lib/daemon');
11
+
12
+ const args = process.argv.slice(2);
13
+ const projectDir = args.find(a => !a.startsWith('-')) || '.';
14
+ const once = args.includes('--once');
15
+
16
+ const daemon = new Daemon(projectDir);
17
+
18
+ if (once) {
19
+ daemon.runOnce().then((result) => {
20
+ process.exit(result.verified && result.compiled ? 0 : 1);
21
+ });
22
+ } else {
23
+ daemon.watch();
24
+ }