lemmafit 0.1.0 → 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.
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
 
@@ -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',
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
@@ -117,6 +121,116 @@ function initProject(targetDir, templateName, serverBase) {
117
121
  console.log('');
118
122
  }
119
123
 
124
+ function addModule(targetDir, moduleName, options = {}) {
125
+ const absTarget = path.resolve(targetDir);
126
+
127
+ const lemmafitDir = path.join(absTarget, 'lemmafit');
128
+ const vibeDir = path.join(lemmafitDir, '.vibe');
129
+ const dafnyDir = path.join(lemmafitDir, 'dafny');
130
+ const configPath = path.join(vibeDir, 'config.json');
131
+ const modulesPath = path.join(vibeDir, 'modules.json');
132
+ const isFirstRun = !fs.existsSync(lemmafitDir);
133
+
134
+ // First run: bootstrap lemmafit infrastructure
135
+ if (isFirstRun) {
136
+ console.log('First lemmafit module — bootstrapping infrastructure...');
137
+ fs.mkdirSync(dafnyDir, { recursive: true });
138
+ fs.mkdirSync(vibeDir, { recursive: true });
139
+
140
+ // Minimal config (no entry/appCore since we use modules.json)
141
+ fs.writeFileSync(configPath, JSON.stringify({}, null, 2) + '\n');
142
+
143
+ // Empty modules array
144
+ fs.writeFileSync(modulesPath, JSON.stringify([], null, 2) + '\n');
145
+
146
+ // Sync .claude/ system files
147
+ syncProject(absTarget);
148
+
149
+ // Add lemmafit as devDependency and daemon script to package.json
150
+ // Create a minimal package.json if one doesn't exist
151
+ const pkgJsonPath = path.join(absTarget, 'package.json');
152
+ if (!fs.existsSync(pkgJsonPath)) {
153
+ const dirName = path.basename(absTarget);
154
+ fs.writeFileSync(pkgJsonPath, JSON.stringify({
155
+ name: dirName,
156
+ private: true
157
+ }, null, 2) + '\n');
158
+ console.log(' Created package.json');
159
+ }
160
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
161
+ if (!pkg.devDependencies) pkg.devDependencies = {};
162
+ if (!pkg.devDependencies.lemmafit && !(pkg.dependencies && pkg.dependencies.lemmafit)) {
163
+ const lemmaPackageDir = path.resolve(__dirname, '..');
164
+ const relPath = path.relative(absTarget, lemmaPackageDir);
165
+ pkg.devDependencies.lemmafit = `file:${relPath}`;
166
+ }
167
+ if (!pkg.scripts) pkg.scripts = {};
168
+ if (!pkg.scripts.daemon) {
169
+ pkg.scripts.daemon = 'lemmafit daemon';
170
+ }
171
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
172
+
173
+ console.log(' Created lemmafit/dafny/');
174
+ console.log(' Created lemmafit/.vibe/config.json');
175
+ console.log(' Created lemmafit/.vibe/modules.json');
176
+ console.log(' Synced .claude/ system files');
177
+ console.log('');
178
+ }
179
+
180
+ if (!moduleName) return;
181
+
182
+ // Load existing modules
183
+ let modules = [];
184
+ if (fs.existsSync(modulesPath)) {
185
+ try { modules = JSON.parse(fs.readFileSync(modulesPath, 'utf8')); } catch {}
186
+ }
187
+
188
+ // Check for duplicate
189
+ if (modules.some(m => m.outputName === moduleName)) {
190
+ console.error(`Error: Module '${moduleName}' already exists in modules.json`);
191
+ process.exit(1);
192
+ }
193
+
194
+ // Scaffold the Dafny file
195
+ const dafnyFile = path.join(dafnyDir, `${moduleName}.dfy`);
196
+ if (fs.existsSync(dafnyFile)) {
197
+ console.error(`Error: ${path.relative(absTarget, dafnyFile)} already exists`);
198
+ process.exit(1);
199
+ }
200
+
201
+ const dafnyContent = `module ${moduleName} {
202
+ // Your verified logic here
203
+ }
204
+ `;
205
+ fs.mkdirSync(dafnyDir, { recursive: true });
206
+ fs.writeFileSync(dafnyFile, dafnyContent);
207
+
208
+ // Add entry to modules.json
209
+ const moduleEntry = {
210
+ entry: `lemmafit/dafny/${moduleName}.dfy`,
211
+ appCore: moduleName,
212
+ outputName: moduleName,
213
+ jsonApi: options.jsonApi !== false,
214
+ nullOptions: options.nullOptions || false
215
+ };
216
+ if (options.target) moduleEntry.target = options.target;
217
+ modules.push(moduleEntry);
218
+ fs.writeFileSync(modulesPath, JSON.stringify(modules, null, 2) + '\n');
219
+
220
+ // Print results
221
+ console.log(` Created lemmafit/dafny/${moduleName}.dfy`);
222
+ console.log(` Added to lemmafit/.vibe/modules.json`);
223
+ console.log('');
224
+ console.log(` Next: write your verified logic in ${moduleName}.dfy`);
225
+ console.log(` The daemon will compile it to src/dafny/${moduleName}.ts`);
226
+ console.log(` Import with: import ${moduleName} from './src/dafny/${moduleName}.ts'`);
227
+ console.log('');
228
+ if (modules.length > 1) {
229
+ console.log(` Modules: ${modules.map(m => m.outputName).join(', ')}`);
230
+ console.log('');
231
+ }
232
+ }
233
+
120
234
  function showLogs(targetDir, clear) {
121
235
  const absTarget = path.resolve(targetDir);
122
236
  const logPath = path.join(absTarget, 'lemmafit', 'logs', 'lemmafit.log');
@@ -197,21 +311,41 @@ function runDaemon(targetDir) {
197
311
  const args = process.argv.slice(2);
198
312
  const command = args[0];
199
313
  const clearFlag = args.includes('--clear');
314
+ const nullOptionsFlag = args.includes('--null-options');
315
+ const noJsonApiFlag = args.includes('--no-json-api');
316
+ const targetIdx = args.indexOf('--target');
317
+ const targetFlag = targetIdx !== -1 ? args[targetIdx + 1] : null;
200
318
  const templateIdx = args.indexOf('--template');
201
319
  const templateName = templateIdx !== -1 ? args[templateIdx + 1] : DEFAULT_TEMPLATE;
202
320
  const serverIdx = args.indexOf('--server');
203
321
  const serverBase = serverIdx !== -1 ? args[serverIdx + 1] : DEFAULT_SERVER;
204
322
  const positionalArgs = args.filter((a, i) =>
205
323
  a !== '--clear' && a !== '--template' && a !== '--server' &&
324
+ a !== '--null-options' && a !== '--no-json-api' && a !== '--target' &&
325
+ (targetIdx === -1 || i !== targetIdx + 1) &&
206
326
  (templateIdx === -1 || i !== templateIdx + 1) &&
207
327
  (serverIdx === -1 || i !== serverIdx + 1)
208
328
  ).slice(1);
209
- const target = positionalArgs[0] || '.';
329
+ let addModuleName = null;
330
+ let target;
331
+ if (command === 'add') {
332
+ addModuleName = positionalArgs[0] || null;
333
+ target = '.';
334
+ } else {
335
+ target = positionalArgs[0] || '.';
336
+ }
210
337
 
211
338
  switch (command) {
212
339
  case 'init':
213
340
  initProject(target, templateName, serverBase);
214
341
  break;
342
+ case 'add':
343
+ addModule(target, addModuleName, {
344
+ jsonApi: !noJsonApiFlag,
345
+ nullOptions: nullOptionsFlag,
346
+ target: targetFlag
347
+ });
348
+ break;
215
349
  case 'sync':
216
350
  syncProject(target);
217
351
  break;
@@ -232,7 +366,11 @@ switch (command) {
232
366
  console.log('Usage:');
233
367
  console.log(' lemmafit init [dir] - Create a new project (blank template)');
234
368
  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)');
369
+ console.log(' lemmafit init --server <url> [dir] - Use a custom server (default: none)');
370
+ console.log(' lemmafit add [Name] - Add a verified module (or just bootstrap infrastructure)');
371
+ console.log(' lemmafit add <Name> --null-options - Add with Option<T> → T | null mapping');
372
+ console.log(' lemmafit add <Name> --no-json-api - Add without JSON marshalling');
373
+ console.log(' lemmafit add <Name> --target <target> - Set compilation target (client|node|inline|deno|cloudflare)');
236
374
  console.log(' lemmafit sync [dir] - Sync system files from package');
237
375
  console.log(' lemmafit daemon [dir] - Run the verification daemon');
238
376
  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:*)",
@@ -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
 
@@ -51,7 +85,7 @@ Load lemmafit-dafny skill before writing or editing any Dafny.
51
85
  Write `.dfy` files in `lemmafit/dafny/` that formalize the verifiable spec entries.
52
86
 
53
87
  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
88
+ - `✓ Verified and compiled` — success, spec queue auto-cleared, wrappers regenerated (`src/dafny/app.ts` or per-module `src/dafny/{name}.ts`)
55
89
  - `✗ Verification failed` — fix the errors shown and write the file again
56
90
 
57
91
  Do not move to the next step until verification passes (verified and compiled).
@@ -76,7 +110,7 @@ Iterate on Steps 4 and 5 until audit returns only minor findings.
76
110
  ## Step 6: Write React code
77
111
  Load lemmafit-react-pattern skill before writing React code.
78
112
 
79
- Only after verification passes. The auto-generated API is at `src/dafny/app.ts` (never edit this file).
113
+ 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
114
 
81
115
  - Create hooks in `src/hooks/` that wrap `Api.Init`, `Api.Dispatch`, `Api.Present`
82
116
  - 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,7 +15,7 @@
15
15
  "verified",
16
16
  "proof"
17
17
  ],
18
- "version": "0.1.0",
18
+ "version": "0.2.0",
19
19
  "type": "commonjs",
20
20
  "files": [
21
21
  "cli/",
@@ -40,9 +40,9 @@
40
40
  "postinstall": "node ./cli/sync.js && node ./cli/download-dafny2js.js && node ./lib/download-dafny.js"
41
41
  },
42
42
  "dependencies": {
43
+ "claimcheck": "^0.2.0",
43
44
  "js-yaml": "^4.1.1",
44
- "ws": "^8.18.0",
45
- "claimcheck": "0.1.0"
45
+ "ws": "^8.18.0"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18.0.0"