lemmafit 0.1.0 → 0.2.1

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,8 +1,10 @@
1
1
  # lemmafit
2
2
 
3
- Make agents **prove** that their code is correct.
3
+ Make agents **prove** that their code is correct.
4
4
 
5
- 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 is written in Dafny, mathematically verified, then auto-compiled to TypeScript for use in your React app.
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
+
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.
6
8
 
7
9
  ## Quick Start
8
10
 
@@ -29,9 +31,9 @@ claude
29
31
 
30
32
  ## Use Cases / Considerations
31
33
 
32
- - lemmafit works with greenfield projects only. You must begin a project with lemmafit. Support for existing codebases is in the pipeline.
34
+ - lemmafit works with greenfield projects. You typically begin a project with `lemmafit init` though `lemmafit add` provides rudimentary support for existing codebases.
33
35
 
34
- - lemmafit compiles Dafny to Typescript which then hooks into a React app. In the future, we will support other languages and frameworks.
36
+ - lemmafit compiles Dafny to Javascript/Typescript which then hooks into a runtime like a React app. In the future, we will support other languages.
35
37
 
36
38
  - lemmafit is optimized to work with Claude Code. In the future, lemmafit will be agent-agnostic.
37
39
 
@@ -42,7 +44,7 @@ claude
42
44
  2. The agent will write a `SPEC.yaml` and write verified logic in `lemmafit/dafny/Domain.dfy`
43
45
  3. The **daemon** watches `.dfy` files, runs `dafny verify`, and on success compiles to `src/dafny/Domain.cjs` + `src/dafny/app.ts`
44
46
  4. The agent will hook the generated TypeScript API into a React app — the logic is proven correct
45
- 5. After proofs complete, run custom command in Claude Code `/guarantees` to activate claimcheck and generate a guarantees report
47
+ 5. After proofs complete, run the `/guarantees` skill to activate claimcheck and generate a guarantees report
46
48
 
47
49
  ## Project Structure
48
50
 
@@ -55,6 +57,7 @@ my-app/
55
57
  │ │ └── Replay.dfy # Generic Replay kernel
56
58
  │ ├── .vibe/
57
59
  │ │ ├── config.json # Project config
60
+ │ │ ├── modules.json # Module registry (for multi-module projects)
58
61
  │ │ ├── status.json # Verification status (generated)
59
62
  │ │ └── claims.json # Proof obligations (generated)
60
63
  │ └── reports/
@@ -73,6 +76,7 @@ my-app/
73
76
 
74
77
  ```bash
75
78
  lemmafit init [dir] # Create project from template
79
+ lemmafit add [Name] # Add a verified module to an existing project
76
80
  lemmafit sync [dir] # Re-sync system files (.claude/, hooks)
77
81
  lemmafit daemon [dir] # Run verification daemon standalone
78
82
  lemmafit logs [dir] # View daemon log
@@ -19,7 +19,7 @@
19
19
  "@types/react": "^18.2.0",
20
20
  "@types/react-dom": "^18.2.0",
21
21
  "@vitejs/plugin-react": "^4.2.0",
22
- "typescript": "^5.3.0",
22
+ "typescript": "^5.3.0",
23
23
  "vite": "^5.0.0"
24
24
  }
25
25
  }
@@ -10,7 +10,7 @@ const path = require('path');
10
10
  const { execSync } = require('child_process');
11
11
  const os = require('os');
12
12
 
13
- const DAFNY2JS_VERSION = '0.9.1';
13
+ const DAFNY2JS_VERSION = '0.9.5';
14
14
 
15
15
  const PLATFORM_RIDS = {
16
16
  'darwin-arm64': 'osx-arm64',
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Generates lemmafit/.vibe/guarantees.md deterministically from:
4
- * - lemmafit/.vibe/guarantees.json (claim-to-spec mapping)
3
+ * Generates reports/guarantees.md deterministically from:
4
+ * - reports/guarantees.json (claim-to-spec mapping)
5
5
  * - lemmafit/.vibe/claimcheck.json (claimcheck results, optional)
6
6
  * - SPEC.yaml (for trusted entries and group info)
7
7
  *
package/cli/lemmafit.js CHANGED
@@ -6,6 +6,10 @@
6
6
  * lemmafit init [dir] - Initialize a new lemmafit project (blank)
7
7
  * lemmafit init --template <name> [dir] - Initialize from a named template
8
8
  * lemmafit init --server <url|none> [dir] - Use a custom server (default: none)
9
+ * lemmafit add [Name] - Add a verified module (or just bootstrap infrastructure)
10
+ * lemmafit add <Name> --null-options - Add with Option<T> → T | null mapping
11
+ * lemmafit add <Name> --no-json-api - Add without JSON marshalling
12
+ * lemmafit add <Name> --target <target> - Set compilation target (client|node|inline|deno|cloudflare)
9
13
  * lemmafit sync [dir] - Sync system files from current package version
10
14
  * lemmafit daemon [dir] - Run the verification daemon
11
15
  * lemmafit logs [dir] - View the dev log
@@ -95,7 +99,8 @@ function initProject(targetDir, templateName, serverBase) {
95
99
  }
96
100
  config.secret = secret;
97
101
  if (serverBase.toLowerCase() !== 'none') {
98
- const serverWsUrl = `${serverBase}/ws?project=${encodeURIComponent(projectName)}`;
102
+ const sep = serverBase.includes('?') ? '&' : '?';
103
+ const serverWsUrl = `${serverBase}${sep}project=${encodeURIComponent(projectName)}`;
99
104
  config.server = serverWsUrl;
100
105
  }
101
106
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -117,6 +122,116 @@ function initProject(targetDir, templateName, serverBase) {
117
122
  console.log('');
118
123
  }
119
124
 
125
+ function addModule(targetDir, moduleName, options = {}) {
126
+ const absTarget = path.resolve(targetDir);
127
+
128
+ const lemmafitDir = path.join(absTarget, 'lemmafit');
129
+ const vibeDir = path.join(lemmafitDir, '.vibe');
130
+ const dafnyDir = path.join(lemmafitDir, 'dafny');
131
+ const configPath = path.join(vibeDir, 'config.json');
132
+ const modulesPath = path.join(vibeDir, 'modules.json');
133
+ const isFirstRun = !fs.existsSync(lemmafitDir);
134
+
135
+ // First run: bootstrap lemmafit infrastructure
136
+ if (isFirstRun) {
137
+ console.log('First lemmafit module — bootstrapping infrastructure...');
138
+ fs.mkdirSync(dafnyDir, { recursive: true });
139
+ fs.mkdirSync(vibeDir, { recursive: true });
140
+
141
+ // Minimal config (no entry/appCore since we use modules.json)
142
+ fs.writeFileSync(configPath, JSON.stringify({}, null, 2) + '\n');
143
+
144
+ // Empty modules array
145
+ fs.writeFileSync(modulesPath, JSON.stringify([], null, 2) + '\n');
146
+
147
+ // Sync .claude/ system files
148
+ syncProject(absTarget);
149
+
150
+ // Add lemmafit as devDependency and daemon script to package.json
151
+ // Create a minimal package.json if one doesn't exist
152
+ const pkgJsonPath = path.join(absTarget, 'package.json');
153
+ if (!fs.existsSync(pkgJsonPath)) {
154
+ const dirName = path.basename(absTarget);
155
+ fs.writeFileSync(pkgJsonPath, JSON.stringify({
156
+ name: dirName,
157
+ private: true
158
+ }, null, 2) + '\n');
159
+ console.log(' Created package.json');
160
+ }
161
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
162
+ if (!pkg.devDependencies) pkg.devDependencies = {};
163
+ if (!pkg.devDependencies.lemmafit && !(pkg.dependencies && pkg.dependencies.lemmafit)) {
164
+ const lemmaPackageDir = path.resolve(__dirname, '..');
165
+ const relPath = path.relative(absTarget, lemmaPackageDir);
166
+ pkg.devDependencies.lemmafit = `file:${relPath}`;
167
+ }
168
+ if (!pkg.scripts) pkg.scripts = {};
169
+ if (!pkg.scripts.daemon) {
170
+ pkg.scripts.daemon = 'lemmafit daemon';
171
+ }
172
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
173
+
174
+ console.log(' Created lemmafit/dafny/');
175
+ console.log(' Created lemmafit/.vibe/config.json');
176
+ console.log(' Created lemmafit/.vibe/modules.json');
177
+ console.log(' Synced .claude/ system files');
178
+ console.log('');
179
+ }
180
+
181
+ if (!moduleName) return;
182
+
183
+ // Load existing modules
184
+ let modules = [];
185
+ if (fs.existsSync(modulesPath)) {
186
+ try { modules = JSON.parse(fs.readFileSync(modulesPath, 'utf8')); } catch {}
187
+ }
188
+
189
+ // Check for duplicate
190
+ if (modules.some(m => m.outputName === moduleName)) {
191
+ console.error(`Error: Module '${moduleName}' already exists in modules.json`);
192
+ process.exit(1);
193
+ }
194
+
195
+ // Scaffold the Dafny file
196
+ const dafnyFile = path.join(dafnyDir, `${moduleName}.dfy`);
197
+ if (fs.existsSync(dafnyFile)) {
198
+ console.error(`Error: ${path.relative(absTarget, dafnyFile)} already exists`);
199
+ process.exit(1);
200
+ }
201
+
202
+ const dafnyContent = `module ${moduleName} {
203
+ // Your verified logic here
204
+ }
205
+ `;
206
+ fs.mkdirSync(dafnyDir, { recursive: true });
207
+ fs.writeFileSync(dafnyFile, dafnyContent);
208
+
209
+ // Add entry to modules.json
210
+ const moduleEntry = {
211
+ entry: `lemmafit/dafny/${moduleName}.dfy`,
212
+ appCore: moduleName,
213
+ outputName: moduleName,
214
+ jsonApi: options.jsonApi !== false,
215
+ nullOptions: options.nullOptions || false
216
+ };
217
+ if (options.target) moduleEntry.target = options.target;
218
+ modules.push(moduleEntry);
219
+ fs.writeFileSync(modulesPath, JSON.stringify(modules, null, 2) + '\n');
220
+
221
+ // Print results
222
+ console.log(` Created lemmafit/dafny/${moduleName}.dfy`);
223
+ console.log(` Added to lemmafit/.vibe/modules.json`);
224
+ console.log('');
225
+ console.log(` Next: write your verified logic in ${moduleName}.dfy`);
226
+ console.log(` The daemon will compile it to src/dafny/${moduleName}.ts`);
227
+ console.log(` Import with: import ${moduleName} from './src/dafny/${moduleName}.ts'`);
228
+ console.log('');
229
+ if (modules.length > 1) {
230
+ console.log(` Modules: ${modules.map(m => m.outputName).join(', ')}`);
231
+ console.log('');
232
+ }
233
+ }
234
+
120
235
  function showLogs(targetDir, clear) {
121
236
  const absTarget = path.resolve(targetDir);
122
237
  const logPath = path.join(absTarget, 'lemmafit', 'logs', 'lemmafit.log');
@@ -197,21 +312,41 @@ function runDaemon(targetDir) {
197
312
  const args = process.argv.slice(2);
198
313
  const command = args[0];
199
314
  const clearFlag = args.includes('--clear');
315
+ const nullOptionsFlag = args.includes('--null-options');
316
+ const noJsonApiFlag = args.includes('--no-json-api');
317
+ const targetIdx = args.indexOf('--target');
318
+ const targetFlag = targetIdx !== -1 ? args[targetIdx + 1] : null;
200
319
  const templateIdx = args.indexOf('--template');
201
320
  const templateName = templateIdx !== -1 ? args[templateIdx + 1] : DEFAULT_TEMPLATE;
202
321
  const serverIdx = args.indexOf('--server');
203
322
  const serverBase = serverIdx !== -1 ? args[serverIdx + 1] : DEFAULT_SERVER;
204
323
  const positionalArgs = args.filter((a, i) =>
205
324
  a !== '--clear' && a !== '--template' && a !== '--server' &&
325
+ a !== '--null-options' && a !== '--no-json-api' && a !== '--target' &&
326
+ (targetIdx === -1 || i !== targetIdx + 1) &&
206
327
  (templateIdx === -1 || i !== templateIdx + 1) &&
207
328
  (serverIdx === -1 || i !== serverIdx + 1)
208
329
  ).slice(1);
209
- const target = positionalArgs[0] || '.';
330
+ let addModuleName = null;
331
+ let target;
332
+ if (command === 'add') {
333
+ addModuleName = positionalArgs[0] || null;
334
+ target = '.';
335
+ } else {
336
+ target = positionalArgs[0] || '.';
337
+ }
210
338
 
211
339
  switch (command) {
212
340
  case 'init':
213
341
  initProject(target, templateName, serverBase);
214
342
  break;
343
+ case 'add':
344
+ addModule(target, addModuleName, {
345
+ jsonApi: !noJsonApiFlag,
346
+ nullOptions: nullOptionsFlag,
347
+ target: targetFlag
348
+ });
349
+ break;
215
350
  case 'sync':
216
351
  syncProject(target);
217
352
  break;
@@ -232,7 +367,11 @@ switch (command) {
232
367
  console.log('Usage:');
233
368
  console.log(' lemmafit init [dir] - Create a new project (blank template)');
234
369
  console.log(' lemmafit init --template <name> [dir] - Create from a named template');
235
- console.log(' lemmafit init --server <url> [dir] - Use a custom server (default: none)');
370
+ console.log(' lemmafit init --server <url> [dir] - Use a custom server (default: none)');
371
+ console.log(' lemmafit add [Name] - Add a verified module (or just bootstrap infrastructure)');
372
+ console.log(' lemmafit add <Name> --null-options - Add with Option<T> → T | null mapping');
373
+ console.log(' lemmafit add <Name> --no-json-api - Add without JSON marshalling');
374
+ console.log(' lemmafit add <Name> --target <target> - Set compilation target (client|node|inline|deno|cloudflare)');
236
375
  console.log(' lemmafit sync [dir] - Sync system files from package');
237
376
  console.log(' lemmafit daemon [dir] - Run the verification daemon');
238
377
  console.log(' lemmafit dashboard [dir] - Open the dashboard in a browser');
package/cli/sync.js CHANGED
@@ -30,9 +30,9 @@ const SETTINGS = {
30
30
  "Read(src/dafny/**)",
31
31
  "Read(node_modules/lemmafit/docs/**)",
32
32
  "Edit(SPEC.yaml)",
33
- "Edit(lemmafit/dafny/Domain.dfy)",
33
+ "Edit(lemmafit/dafny/*.dfy)",
34
34
  "Write(SPEC.yaml)",
35
- "Write(lemmafit/dafny/**)",
35
+ "Write(lemmafit/dafny/*.dfy)",
36
36
  // Common build/dev commands
37
37
  "Bash(npm run build:*)",
38
38
  "Bash(npm run dev:*)",
@@ -110,19 +110,6 @@ function syncProject(targetDir) {
110
110
  fs.writeFileSync(claudeMdPath, POINTER_LINE + '\n');
111
111
  }
112
112
 
113
- // Sync .claude/commands/ from package
114
- const srcCommands = path.join(__dirname, '..', 'commands');
115
- if (fs.existsSync(srcCommands)) {
116
- const commandsDir = path.join(claudeDir, 'commands');
117
- fs.mkdirSync(commandsDir, { recursive: true });
118
- for (const file of fs.readdirSync(srcCommands)) {
119
- fs.copyFileSync(
120
- path.join(srcCommands, file),
121
- path.join(commandsDir, file)
122
- );
123
- }
124
- }
125
-
126
113
  // Sync .claude/skills/ from package
127
114
  const srcSkills = path.join(__dirname, '..', 'skills');
128
115
  if (fs.existsSync(srcSkills)) {
@@ -19,6 +19,40 @@ Before writing code, ask yourself: "Is any part of this code verifiable?" Verifi
19
19
  - `src/App.tsx` - React app that uses the verified API
20
20
  - `lemmafit/.vibe/status.json` - Current verification status
21
21
  - `lemmafit/.vibe/logic-surface.json` - Logic interface/API
22
+ - `lemmafit/.vibe/modules.json` - Multi-module configuration (optional, see below)
23
+
24
+ ## Multi-Module Projects
25
+
26
+ By default, lemmafit uses a single Dafny module with the Replay kernel pattern (Domain.dfy → app.ts). For projects that need multiple independent verified modules, create `lemmafit/.vibe/modules.json`:
27
+
28
+ ```json
29
+ [
30
+ {
31
+ "entry": "lemmafit/dafny/Workflow.dfy",
32
+ "appCore": "Workflow",
33
+ "outputName": "Workflow",
34
+ "jsonApi": true
35
+ },
36
+ {
37
+ "entry": "lemmafit/dafny/Validation.dfy",
38
+ "appCore": "Validation",
39
+ "outputName": "Validation",
40
+ "jsonApi": true,
41
+ "nullOptions": true,
42
+ "target": "node"
43
+ }
44
+ ]
45
+ ```
46
+
47
+ When `modules.json` exists:
48
+ - Each module is compiled independently to `src/dafny/{outputName}.cjs` and `src/dafny/{outputName}.ts`
49
+ - Each module is its own AppCore (no separate AppCore module needed)
50
+ - `jsonApi: true` enables full JSON marshalling (plain types in/out, no Dafny runtime types)
51
+ - `nullOptions: true` maps `Option<T>` to `T | null` at the boundary
52
+ - `target` sets the dafny2js compilation target (default: `"client"`). Valid values: `"client"` (browser/React), `"node"` (Node.js, uses `fs.readFileSync`), `"inline"` (universal, inlines .cjs code), `"deno"` (Deno adapter), `"cloudflare"` (Cloudflare Workers adapter)
53
+ - Modules don't know about each other — write a thin TypeScript glue file to connect them
54
+ - The glue file is unverified but should be minimal and auditable
55
+ - Prefer returning result types with verified error messages over boolean predicates — the UI can display them directly without duplicating logic
22
56
 
23
57
  ## Available Skills
24
58
 
@@ -26,6 +60,10 @@ Before writing code, ask yourself: "Is any part of this code verifiable?" Verifi
26
60
  - `lemmafit-proofs`: Load this skill before writing or editing lemmas
27
61
  - `lemmafit-react-pattern`: Load this skill before writing React
28
62
  - `lemmafit-spec`: Load this skill when user asks to add or edit feature, and before writing or editing the spec.yaml file
63
+ - `lemmafit-guarantees`: Load this skill to generate a human-readable guarantees report from proven Dafny code and verify claims with claimcheck
64
+ - `lemmafit-pre-react-audits`: Load this skill before writing React to audit proof strength and catch unverified logic
65
+ - `lemmafit-post-react-audit`: Load this skill after writing React to catch effect-free logic that should be in Dafny
66
+
29
67
 
30
68
  If you try to read any of these files and they are missing, alert the user.
31
69
 
@@ -51,7 +89,7 @@ Load lemmafit-dafny skill before writing or editing any Dafny.
51
89
  Write `.dfy` files in `lemmafit/dafny/` that formalize the verifiable spec entries.
52
90
 
53
91
  A hook runs automatically after you write any `.dfy` file — it verifies and compiles immediately. You must wait for a response from the daemon before moving forward. The response will be one of two:
54
- - `✓ Verified and compiled` — success, spec queue auto-cleared, `src/dafny/app.ts` regenerated
92
+ - `✓ Verified and compiled` — success, spec queue auto-cleared, wrappers regenerated (`src/dafny/app.ts` or per-module `src/dafny/{name}.ts`)
55
93
  - `✗ Verification failed` — fix the errors shown and write the file again
56
94
 
57
95
  Do not move to the next step until verification passes (verified and compiled).
@@ -76,7 +114,7 @@ Iterate on Steps 4 and 5 until audit returns only minor findings.
76
114
  ## Step 6: Write React code
77
115
  Load lemmafit-react-pattern skill before writing React code.
78
116
 
79
- Only after verification passes. The auto-generated API is at `src/dafny/app.ts` (never edit this file).
117
+ Only after verification passes. The auto-generated API is at `src/dafny/app.ts` (single-module) or `src/dafny/{name}.ts` (multi-module). Never edit generated files.
80
118
 
81
119
  - Create hooks in `src/hooks/` that wrap `Api.Init`, `Api.Dispatch`, `Api.Present`
82
120
  - Create components in `src/components/` that receive data/callbacks via props
package/lib/daemon.js CHANGED
@@ -38,6 +38,8 @@ class Daemon {
38
38
 
39
39
  // Load or create config
40
40
  this.config = this.loadConfig();
41
+ this.modulesPath = path.join(this.vibeDir, 'modules.json');
42
+ this.modules = this.loadModules();
41
43
 
42
44
  // WS relay state (only active when config.server is set)
43
45
  this.relayWs = null;
@@ -96,6 +98,27 @@ class Daemon {
96
98
  return defaultConfig;
97
99
  }
98
100
 
101
+ loadModules() {
102
+ if (fs.existsSync(this.modulesPath)) {
103
+ try {
104
+ const modules = JSON.parse(fs.readFileSync(this.modulesPath, 'utf8'));
105
+ if (Array.isArray(modules) && modules.length > 0) {
106
+ return modules;
107
+ }
108
+ } catch {}
109
+ }
110
+ // Fall back to single-module from config.json
111
+ return [{
112
+ entry: this.config.entry,
113
+ appCore: this.config.appCore,
114
+ outputName: this.config.outputName
115
+ }];
116
+ }
117
+
118
+ isMultiModule() {
119
+ return fs.existsSync(this.modulesPath);
120
+ }
121
+
99
122
  readSpecFile() {
100
123
  const specPath = path.join(this.projectDir, 'SPEC.yaml');
101
124
  if (!fs.existsSync(specPath)) return null;
@@ -203,6 +226,10 @@ class Daemon {
203
226
  hash.update(fs.readFileSync(file, 'utf8'));
204
227
  } catch {}
205
228
  }
229
+ // Include modules.json so config changes trigger recompilation
230
+ try {
231
+ hash.update(fs.readFileSync(this.modulesPath, 'utf8'));
232
+ } catch {}
206
233
  return hash.digest('hex');
207
234
  }
208
235
 
@@ -350,15 +377,33 @@ class Daemon {
350
377
  }
351
378
 
352
379
  async compile() {
353
- const entryPath = path.join(this.projectDir, this.config.entry);
380
+ // Reload modules in case modules.json was created/changed
381
+ this.modules = this.loadModules();
382
+
383
+ const errors = [];
384
+ for (const mod of this.modules) {
385
+ const result = await this.compileModule(mod);
386
+ if (!result.success) {
387
+ errors.push(`${mod.outputName}: ${result.error}`);
388
+ }
389
+ }
390
+
391
+ if (errors.length > 0) {
392
+ return { success: false, error: errors.join('\n') };
393
+ }
394
+ return { success: true };
395
+ }
396
+
397
+ async compileModule(mod) {
398
+ const entryPath = path.join(this.projectDir, mod.entry);
354
399
  if (!fs.existsSync(entryPath)) {
355
- return { success: false, error: `Entry file not found: ${this.config.entry}` };
400
+ return { success: false, error: `Entry file not found: ${mod.entry}` };
356
401
  }
357
402
 
358
403
  const generatedDir = path.join(this.projectDir, 'generated');
359
404
  fs.mkdirSync(generatedDir, { recursive: true });
360
405
 
361
- const outputBase = path.join(generatedDir, this.config.outputName);
406
+ const outputBase = path.join(generatedDir, mod.outputName);
362
407
 
363
408
  // Step 1: dafny translate js
364
409
  const translateResult = await this.runCommand(this.dafnyPath, [
@@ -379,22 +424,38 @@ class Daemon {
379
424
  return { success: false, error: `Generated JS not found: ${generatedJs}` };
380
425
  }
381
426
 
382
- const targetCjs = path.join(this.srcDafnyDir, `${this.config.outputName}.cjs`);
427
+ const outputDir = mod.outputDir
428
+ ? path.resolve(this.projectDir, mod.outputDir)
429
+ : this.srcDafnyDir;
430
+ fs.mkdirSync(outputDir, { recursive: true });
431
+
432
+ const targetCjs = path.join(outputDir, `${mod.outputName}.cjs`);
383
433
  fs.copyFileSync(generatedJs, targetCjs);
384
434
 
385
- // Step 3: Run dafny2js to generate app.ts
435
+ // Step 3: Run dafny2js to generate wrapper
386
436
  if (!fs.existsSync(this.dafny2jsBin)) {
387
437
  return { success: false, error: `dafny2js not found at ${this.dafny2jsBin}` };
388
438
  }
389
439
 
390
- const appTsPath = path.join(this.srcDafnyDir, 'app.ts');
440
+ // Multi-module: each module gets its own {outputName}.ts
441
+ // Single-module (backward compat): output is app.ts
442
+ const wrapperName = this.isMultiModule()
443
+ ? `${mod.outputName}.ts`
444
+ : 'app.ts';
445
+ const wrapperPath = path.join(outputDir, wrapperName);
391
446
 
392
- const dafny2jsResult = await this.runCommand(this.dafny2jsBin, [
447
+ const targetFlag = `--${mod.target || 'client'}`;
448
+ const dafny2jsArgs = [
393
449
  '--file', entryPath,
394
- '--app-core', this.config.appCore,
395
- '--cjs-name', `${this.config.outputName}.cjs`,
396
- '--client', appTsPath
397
- ]);
450
+ '--app-core', mod.appCore,
451
+ '--cjs-name', `${mod.outputName}.cjs`,
452
+ targetFlag, wrapperPath
453
+ ];
454
+
455
+ if (mod.jsonApi) dafny2jsArgs.push('--json-api');
456
+ if (mod.nullOptions) dafny2jsArgs.push('--null-options');
457
+
458
+ const dafny2jsResult = await this.runCommand(this.dafny2jsBin, dafny2jsArgs);
398
459
 
399
460
  if (dafny2jsResult.code !== 0) {
400
461
  return { success: false, error: `dafny2js failed: ${dafny2jsResult.stderr || dafny2jsResult.stdout}` };
@@ -404,60 +465,69 @@ class Daemon {
404
465
  }
405
466
 
406
467
  async extractClaims() {
407
- const entryPath = path.join(this.projectDir, this.config.entry);
408
- if (!fs.existsSync(entryPath)) {
409
- return { success: false, error: `Entry file not found: ${this.config.entry}` };
410
- }
411
-
412
468
  const claimsPath = path.join(this.vibeDir, 'claims.json');
469
+ const allClaims = { axioms: [], lemmas: [], predicates: [], functions: [] };
413
470
 
414
- const result = await this.runCommand(this.dafny2jsBin, [
415
- '--file', entryPath,
416
- '--claims'
417
- ]);
418
-
419
- if (result.code !== 0) {
420
- return { success: false, error: `claims extraction failed: ${result.stderr || result.stdout}` };
421
- }
471
+ for (const mod of this.modules) {
472
+ const entryPath = path.join(this.projectDir, mod.entry);
473
+ if (!fs.existsSync(entryPath)) continue;
422
474
 
423
- // Parse and write claims.json
424
- try {
425
- const claims = JSON.parse(result.stdout);
426
- fs.writeFileSync(claimsPath, JSON.stringify(claims, null, 2));
475
+ const result = await this.runCommand(this.dafny2jsBin, [
476
+ '--file', entryPath,
477
+ '--claims'
478
+ ]);
427
479
 
428
- // Generate ASSUMPTIONS.md from axioms
429
- this.generateAssumptions(claims);
480
+ if (result.code !== 0) {
481
+ return { success: false, error: `claims extraction failed for ${mod.outputName}: ${result.stderr || result.stdout}` };
482
+ }
430
483
 
431
- return { success: true, claims };
432
- } catch (err) {
433
- return { success: false, error: `Failed to parse claims JSON: ${err.message}` };
484
+ try {
485
+ const claims = JSON.parse(result.stdout);
486
+ for (const key of Object.keys(allClaims)) {
487
+ if (claims[key]) allClaims[key].push(...claims[key]);
488
+ }
489
+ } catch (err) {
490
+ return { success: false, error: `Failed to parse claims JSON for ${mod.outputName}: ${err.message}` };
491
+ }
434
492
  }
493
+
494
+ fs.writeFileSync(claimsPath, JSON.stringify(allClaims, null, 2));
495
+ this.generateAssumptions(allClaims);
496
+ return { success: true, claims: allClaims };
435
497
  }
436
498
 
437
499
  async extractLogicSurface() {
438
- const entryPath = path.join(this.projectDir, this.config.entry);
439
- if (!fs.existsSync(entryPath)) {
440
- return { success: false, error: `Entry file not found: ${this.config.entry}` };
441
- }
500
+ const allSurfaces = [];
442
501
 
443
- const result = await this.runCommand(this.dafny2jsBin, [
444
- '--file', entryPath,
445
- '--logic-surface',
446
- '--app-core', this.config.appCore || 'AppCore'
447
- ]);
502
+ for (const mod of this.modules) {
503
+ const entryPath = path.join(this.projectDir, mod.entry);
504
+ if (!fs.existsSync(entryPath)) continue;
448
505
 
449
- if (result.code !== 0) {
450
- return { success: false, error: `logic surface extraction failed: ${result.stderr || result.stdout}` };
451
- }
506
+ const result = await this.runCommand(this.dafny2jsBin, [
507
+ '--file', entryPath,
508
+ '--logic-surface',
509
+ '--app-core', mod.appCore
510
+ ]);
452
511
 
453
- try {
454
- const surface = JSON.parse(result.stdout);
455
- const surfacePath = path.join(this.vibeDir, 'logic-surface.json');
456
- fs.writeFileSync(surfacePath, JSON.stringify(surface, null, 2));
457
- return { success: true, surface };
458
- } catch (err) {
459
- return { success: false, error: `Failed to parse logic surface JSON: ${err.message}` };
512
+ if (result.code !== 0) {
513
+ return { success: false, error: `logic surface extraction failed for ${mod.outputName}: ${result.stderr || result.stdout}` };
514
+ }
515
+
516
+ try {
517
+ const surface = JSON.parse(result.stdout);
518
+ surface._module = mod.outputName;
519
+ allSurfaces.push(surface);
520
+ } catch (err) {
521
+ return { success: false, error: `Failed to parse logic surface JSON for ${mod.outputName}: ${err.message}` };
522
+ }
460
523
  }
524
+
525
+ // Single module: write surface directly (backward compat)
526
+ // Multi-module: write array of surfaces
527
+ const output = allSurfaces.length === 1 ? allSurfaces[0] : allSurfaces;
528
+ const surfacePath = path.join(this.vibeDir, 'logic-surface.json');
529
+ fs.writeFileSync(surfacePath, JSON.stringify(output, null, 2));
530
+ return { success: true, surface: output };
461
531
  }
462
532
 
463
533
  generateAssumptions(claims) {
@@ -765,7 +835,11 @@ class Daemon {
765
835
  console.log(`Lemmafit daemon watching: ${this.projectDir}`);
766
836
  console.log(` Dafny files: ${this.dafnyDir}`);
767
837
  console.log(` Status: ${this.statusPath}`);
768
- console.log(` Entry: ${this.config.entry}`);
838
+ if (this.isMultiModule()) {
839
+ console.log(` Modules: ${this.modules.map(m => m.outputName).join(', ')}`);
840
+ } else {
841
+ console.log(` Entry: ${this.config.entry}`);
842
+ }
769
843
  console.log(` Dafny: ${this.dafnyPath}`);
770
844
  if (this.config.server) {
771
845
  console.log(` Relay: ${this.config.server}`);
package/package.json CHANGED
@@ -15,14 +15,13 @@
15
15
  "verified",
16
16
  "proof"
17
17
  ],
18
- "version": "0.1.0",
18
+ "version": "0.2.1",
19
19
  "type": "commonjs",
20
20
  "files": [
21
21
  "cli/",
22
22
  "lib/",
23
23
  "docs/",
24
24
  "skills/",
25
- "commands/",
26
25
  "kernels/",
27
26
  "blank-template/"
28
27
  ],
@@ -40,9 +39,9 @@
40
39
  "postinstall": "node ./cli/sync.js && node ./cli/download-dafny2js.js && node ./lib/download-dafny.js"
41
40
  },
42
41
  "dependencies": {
42
+ "claimcheck": "^0.2.0",
43
43
  "js-yaml": "^4.1.1",
44
- "ws": "^8.18.0",
45
- "claimcheck": "0.1.0"
44
+ "ws": "^8.18.0"
46
45
  },
47
46
  "engines": {
48
47
  "node": ">=18.0.0"
@@ -6,7 +6,7 @@ description: Dafny code patterns and reference for lemmafit apps. Use when writi
6
6
  # Lemmafit Dafny
7
7
 
8
8
  ## When to Write Code in Dafny
9
- - ALL `verifiable:true` entries in the spec MUST be written in Dafny (do not write verifiable code directly in JavaScript or TypeSscript)
9
+ - ALL `verifiable:true` entries in the spec MUST be written in Dafny (do not write verifiable code directly in JavaScript or TypeScript)
10
10
 
11
11
  ## Dafny Pattern Example
12
12
 
@@ -1,10 +1,15 @@
1
+ ---
2
+ name: lemmafit-guarantees
3
+ description: Generate human-readable guarantees from proven Dafny code and verify them with claimcheck. Use after verification succeeds and SPEC.yaml is in sync with Dafny. Produces guarantees.json, claimcheck-mapping.json, runs claimcheck-multi, and generates guarantees.md report.
4
+ ---
5
+
1
6
  # Generate Guarantees and Run Claimcheck
2
7
 
3
8
  You are generating human-readable guarantees from proven Dafny code and verifying them with claimcheck.
4
9
 
5
10
  ## Step 0: Make sure the state of the project is verified
6
11
 
7
- Check `lemmafit/.vibe/status.json` for the current verification status.
12
+ Check your context for verification status of the project.
8
13
 
9
14
  ## Step 1: Read project data
10
15
 
@@ -35,7 +40,7 @@ Analyze the claims from `claims.json` and map them to spec entries from `SPEC.ya
35
40
 
36
41
  ## Step 3: Write guarantees.json
37
42
 
38
- Write `lemmafit/.vibe/guarantees.json` with this format:
43
+ Write `reports/guarantees.json` with this format:
39
44
 
40
45
  ```json
41
46
  {
@@ -117,15 +122,15 @@ Parse the claimcheck results and report:
117
122
  - **Disputed** — a discrepancy was found. Show the `discrepancy` text and `weakeningType` (tautology, weakened-postcondition, narrowed-scope, wrong-property).
118
123
  - **Error** — lemma not found in source. Check the lemmaName.
119
124
 
120
- **If any claims are disputed:** Suggest specific fixes to the Dafny code or the requirement text. If the user agrees, make the fixes, wait for re-verification, and re-run the guarantees process.
125
+ **If any claims are disputed:** Suggest specific fixes to the Dafny code or the requirement text. If the user agrees, make the fixes, wait for re-verification, and re-run the guarantees process.
121
126
 
122
127
  ## Step 8: Ensure all files up to date
123
128
 
124
- Once iteration is complete, compare `claimcheck-mapping.json` with `guarantees.json` to ensure they contain equivalent information. If there's a discrepancy, trace back to the Dafny code to find which is most accurate. Adjust the relevant file accordingly and re-run `/guarantees` command. Once confirmed, report that the files are in sync.
129
+ Once iteration is complete, compare `claimcheck-mapping.json` with `guarantees.json` to ensure they contain equivalent information. If there's a discrepancy, trace back to the Dafny code to find which is most accurate. Adjust the relevant file accordingly and re-run `/guarantees` command. Once confirmed, report that the files are in sync.
125
130
 
126
131
  ## Step 9: Generate guarantees.md via the script
127
132
 
128
- Do this only after Step 9 confirms that `claimcheck-mapping.json` and `guarantees.json` are in sync.
133
+ Do this only after Step 8 confirms that `claimcheck-mapping.json` and `guarantees.json` are in sync.
129
134
 
130
135
  Run the deterministic report generator:
131
136
 
@@ -133,6 +138,6 @@ Run the deterministic report generator:
133
138
  npx lemmafit-generate-guarantees
134
139
  ```
135
140
 
136
- This reads `lemmafit/.vibe/guarantees.json`, `lemmafit/.vibe/claimcheck.json`, and `SPEC.yaml` and writes `lemmafit/.vibe/guarantees.md`. Do NOT write this file manually — always use the script so the report matches the JSON exactly.
141
+ This reads `reports/guarantees.json`, `reports/claimcheck.json`, and `SPEC.yaml` and writes `reports/guarantees.md`. Do NOT write this file manually — always use the script so the report matches the JSON exactly.
137
142
 
138
- Report to the user: "A report of your app's guarantees has been generated in lemmafit/reports/guarantees.md"
143
+ Report to the user: "A report of your app's guarantees has been generated in lemmafit/reports/guarantees.md"
@@ -39,7 +39,7 @@ SPEC.yaml is a structured YAML file with an `entries` list. Each entry has:
39
39
  - `depends_on` — list of spec IDs this entry depends on
40
40
  - `verifiable` — whether this can be proven in Dafny
41
41
  - `guarantee_type` — `verified`, `assumed`, or `trusted`
42
- - `state` - `DRAFT`, `ADDRESSED`, `null`- only use `addressed` if the corresponding Dafny module or property have been verified. Use null for `verifiable: false`
42
+ - `state` - `DRAFT`, `ADDRESSED`, `null` only use `ADDRESSED` if the corresponding Dafny module or property have been verified. Use `null` for `verifiable: false`
43
43
 
44
44
  ### Example
45
45
  ```yaml
@@ -54,7 +54,8 @@ entries:
54
54
  module: AppCore
55
55
  depends_on: []
56
56
  verifiable: true
57
- status: verified
57
+ guarantee_type: verified
58
+ state: ADDRESSED
58
59
  - id: spec-002
59
60
  req_id: null
60
61
  title: The increment button displays the current count