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 +4 -2
- package/cli/download-dafny2js.js +1 -1
- package/cli/lemmafit.js +140 -2
- package/cli/sync.js +2 -2
- package/docs/CLAUDE_INSTRUCTIONS.md +36 -2
- package/lib/daemon.js +127 -53
- package/package.json +3 -3
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
|
-
|
|
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
|
|
package/cli/download-dafny2js.js
CHANGED
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
|
-
|
|
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]
|
|
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
|
|
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`
|
|
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` (
|
|
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
|
-
|
|
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: ${
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
447
|
+
const targetFlag = `--${mod.target || 'client'}`;
|
|
448
|
+
const dafny2jsArgs = [
|
|
393
449
|
'--file', entryPath,
|
|
394
|
-
'--app-core',
|
|
395
|
-
'--cjs-name', `${
|
|
396
|
-
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
475
|
+
const result = await this.runCommand(this.dafny2jsBin, [
|
|
476
|
+
'--file', entryPath,
|
|
477
|
+
'--claims'
|
|
478
|
+
]);
|
|
427
479
|
|
|
428
|
-
|
|
429
|
-
|
|
480
|
+
if (result.code !== 0) {
|
|
481
|
+
return { success: false, error: `claims extraction failed for ${mod.outputName}: ${result.stderr || result.stdout}` };
|
|
482
|
+
}
|
|
430
483
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
506
|
+
const result = await this.runCommand(this.dafny2jsBin, [
|
|
507
|
+
'--file', entryPath,
|
|
508
|
+
'--logic-surface',
|
|
509
|
+
'--app-core', mod.appCore
|
|
510
|
+
]);
|
|
452
511
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|