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 +9 -5
- package/blank-template/package.json +1 -1
- package/cli/download-dafny2js.js +1 -1
- package/cli/generate-guarantees-md.js +2 -2
- package/cli/lemmafit.js +142 -3
- package/cli/sync.js +2 -15
- package/docs/CLAUDE_INSTRUCTIONS.md +40 -2
- package/lib/daemon.js +127 -53
- package/package.json +3 -4
- package/skills/lemmafit-dafny/SKILL.md +1 -1
- package/{commands/guarantees.md → skills/lemmafit-guarantees/SKILL.md} +12 -7
- package/skills/lemmafit-spec/SKILL.md +3 -2
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
|
|
|
@@ -29,9 +31,9 @@ claude
|
|
|
29
31
|
|
|
30
32
|
## Use Cases / Considerations
|
|
31
33
|
|
|
32
|
-
- lemmafit works with greenfield projects
|
|
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
|
|
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
|
|
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
|
package/cli/download-dafny2js.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Generates
|
|
4
|
-
* -
|
|
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
|
|
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
|
-
|
|
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]
|
|
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
|
|
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`
|
|
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` (
|
|
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
|
-
|
|
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,14 +15,13 @@
|
|
|
15
15
|
"verified",
|
|
16
16
|
"proof"
|
|
17
17
|
],
|
|
18
|
-
"version": "0.1
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|