lemmafit 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -4
- package/blank-template/README.md +3 -0
- package/blank-template/SPEC.yaml +1 -0
- package/blank-template/index.html +12 -0
- package/blank-template/lemmafit/.vibe/config.json +5 -0
- package/blank-template/lemmafit/dafny/Domain.dfy +5 -0
- package/blank-template/lemmafit/dafny/Replay.dfy +147 -0
- package/blank-template/package.json +25 -0
- package/blank-template/src/App.css +3 -0
- package/blank-template/src/App.tsx +10 -0
- package/blank-template/src/dafny/.gitkeep +0 -0
- package/blank-template/src/index.css +29 -0
- package/blank-template/src/main.tsx +10 -0
- package/blank-template/src/vite-env.d.ts +6 -0
- package/blank-template/template.gitignore +3 -0
- package/blank-template/tsconfig.json +21 -0
- package/blank-template/tsconfig.node.json +11 -0
- package/blank-template/vite.config.js +9 -0
- package/cli/context-hook.js +103 -0
- package/cli/daemon.js +24 -0
- package/cli/download-dafny2js.js +136 -0
- package/cli/generate-guarantees-md.js +223 -0
- package/cli/lemmafit.js +385 -0
- package/cli/session-hook.js +74 -0
- package/cli/sync.js +168 -0
- package/cli/verify-hook.js +221 -0
- package/commands/guarantees.md +138 -0
- package/docs/CLAUDE_INSTRUCTIONS.md +137 -0
- package/kernels/Replay.dfy +147 -0
- package/lib/daemon-client.js +54 -0
- package/lib/daemon.js +990 -0
- package/lib/download-dafny.js +130 -0
- package/lib/log.js +32 -0
- package/lib/spawn-claude.js +51 -0
- package/package.json +49 -5
- package/skills/lemmafit-dafny/SKILL.md +101 -0
- package/skills/lemmafit-post-react-audit/SKILL.md +46 -0
- package/skills/lemmafit-pre-react-audits/SKILL.md +67 -0
- package/skills/lemmafit-proofs/SKILL.md +24 -0
- package/skills/lemmafit-react-pattern/SKILL.md +62 -0
- package/skills/lemmafit-spec/SKILL.md +71 -0
- package/index.js +0 -5
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Downloads Dafny binary for the current platform.
|
|
4
|
+
* Called during npm postinstall.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
const DAFNY_VERSION = '4.11.0';
|
|
14
|
+
|
|
15
|
+
const PLATFORM_ASSETS = {
|
|
16
|
+
'darwin-arm64': `dafny-${DAFNY_VERSION}-arm64-macos-13.zip`,
|
|
17
|
+
'darwin-x64': `dafny-${DAFNY_VERSION}-x64-macos-13.zip`,
|
|
18
|
+
'linux-x64': `dafny-${DAFNY_VERSION}-x64-ubuntu-22.04.zip`,
|
|
19
|
+
'linux-arm64': `dafny-${DAFNY_VERSION}-arm64-ubuntu-22.04.zip`,
|
|
20
|
+
'win32-x64': `dafny-${DAFNY_VERSION}-x64-windows-2019.zip`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getPlatformKey() {
|
|
24
|
+
const platform = os.platform();
|
|
25
|
+
const arch = os.arch();
|
|
26
|
+
return `${platform}-${arch}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function download(url, dest) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const file = fs.createWriteStream(dest);
|
|
32
|
+
|
|
33
|
+
const request = (url) => {
|
|
34
|
+
https.get(url, (response) => {
|
|
35
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
36
|
+
// Follow redirect
|
|
37
|
+
request(response.headers.location);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (response.statusCode !== 200) {
|
|
42
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const total = parseInt(response.headers['content-length'], 10);
|
|
47
|
+
let downloaded = 0;
|
|
48
|
+
|
|
49
|
+
response.on('data', (chunk) => {
|
|
50
|
+
downloaded += chunk.length;
|
|
51
|
+
const pct = total ? Math.round((downloaded / total) * 100) : '?';
|
|
52
|
+
process.stdout.write(`\rDownloading Dafny... ${pct}%`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
response.pipe(file);
|
|
56
|
+
|
|
57
|
+
file.on('finish', () => {
|
|
58
|
+
file.close();
|
|
59
|
+
console.log(' Done.');
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
}).on('error', (err) => {
|
|
63
|
+
fs.unlink(dest, () => {});
|
|
64
|
+
reject(err);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
request(url);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
const platformKey = getPlatformKey();
|
|
74
|
+
const asset = PLATFORM_ASSETS[platformKey];
|
|
75
|
+
|
|
76
|
+
if (!asset) {
|
|
77
|
+
console.error(`Unsupported platform: ${platformKey}`);
|
|
78
|
+
console.error('Supported platforms:', Object.keys(PLATFORM_ASSETS).join(', '));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Install to shared cache directory (~/.lemmafit/.dafny)
|
|
83
|
+
const cacheDir = path.join(os.homedir(), '.lemmafit');
|
|
84
|
+
const installDir = path.join(cacheDir, '.dafny');
|
|
85
|
+
const dafnyDir = path.join(installDir, 'dafny');
|
|
86
|
+
const dafnyBin = path.join(dafnyDir, 'dafny');
|
|
87
|
+
const versionFile = path.join(installDir, 'version');
|
|
88
|
+
|
|
89
|
+
// Check if correct version is already installed
|
|
90
|
+
if (fs.existsSync(dafnyBin) && fs.existsSync(versionFile)) {
|
|
91
|
+
const installed = fs.readFileSync(versionFile, 'utf8').trim();
|
|
92
|
+
if (installed === DAFNY_VERSION) {
|
|
93
|
+
console.log(`Dafny ${DAFNY_VERSION} already installed.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(`Dafny ${installed} -> ${DAFNY_VERSION}, upgrading...`);
|
|
97
|
+
fs.rmSync(dafnyDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create install directory
|
|
101
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
// Download
|
|
104
|
+
const url = `https://github.com/dafny-lang/dafny/releases/download/v${DAFNY_VERSION}/${asset}`;
|
|
105
|
+
const zipPath = path.join(installDir, asset);
|
|
106
|
+
|
|
107
|
+
console.log(`Downloading Dafny ${DAFNY_VERSION} for ${platformKey}...`);
|
|
108
|
+
await download(url, zipPath);
|
|
109
|
+
|
|
110
|
+
// Extract
|
|
111
|
+
console.log('Extracting...');
|
|
112
|
+
execSync(`unzip -q -o "${zipPath}" -d "${installDir}"`, { stdio: 'inherit' });
|
|
113
|
+
|
|
114
|
+
// Clean up zip
|
|
115
|
+
fs.unlinkSync(zipPath);
|
|
116
|
+
|
|
117
|
+
// Make executable
|
|
118
|
+
if (os.platform() !== 'win32') {
|
|
119
|
+
fs.chmodSync(dafnyBin, '755');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(versionFile, DAFNY_VERSION);
|
|
123
|
+
console.log(`Dafny ${DAFNY_VERSION} installed to ${dafnyDir}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch((err) => {
|
|
127
|
+
console.error('Failed to download Dafny:', err.message);
|
|
128
|
+
console.error('You may need to install Dafny manually: https://github.com/dafny-lang/dafny/releases');
|
|
129
|
+
// Don't exit with error - allow npm install to continue
|
|
130
|
+
});
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared logger for lemmafit hooks and scripts.
|
|
3
|
+
* Appends timestamped lines to logs/lemmafit.log in the project root.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
let _logPath = null;
|
|
10
|
+
|
|
11
|
+
function initLog(projectDir) {
|
|
12
|
+
const logsDir = path.join(projectDir, 'lemmafit', 'logs');
|
|
13
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
14
|
+
_logPath = path.join(logsDir, 'lemmafit.log');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function log(source, message) {
|
|
18
|
+
if (!_logPath) return;
|
|
19
|
+
const ts = new Date().toISOString();
|
|
20
|
+
const line = `[${ts}] [${source}] ${message}\n`;
|
|
21
|
+
try {
|
|
22
|
+
fs.appendFileSync(_logPath, line);
|
|
23
|
+
} catch {
|
|
24
|
+
// Never let logging break the caller
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getLogPath() {
|
|
29
|
+
return _logPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { initLog, log, getLogPath };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared spawnClaude helper — runs `claude -p` as a subprocess.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the daemon (WS relay commands) and the dashboard (vite plugin).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
10
|
+
const ALLOWED_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
11
|
+
const MAX_TURNS_LIMIT = 10;
|
|
12
|
+
|
|
13
|
+
function spawnClaude(prompt, cwd, { model = 'haiku', maxTurns = 1, effort = 'low' } = {}) {
|
|
14
|
+
if (typeof prompt !== 'string' || prompt.length === 0) {
|
|
15
|
+
return Promise.reject(new Error('prompt must be a non-empty string'));
|
|
16
|
+
}
|
|
17
|
+
if (!ALLOWED_MODELS.has(model)) model = 'haiku';
|
|
18
|
+
if (!ALLOWED_EFFORTS.has(effort)) effort = 'low';
|
|
19
|
+
maxTurns = Math.max(1, Math.min(MAX_TURNS_LIMIT, parseInt(maxTurns, 10) || 1));
|
|
20
|
+
|
|
21
|
+
const args = ['-p', prompt, '--output-format', 'text', '--max-turns', String(maxTurns), '--tools', ''];
|
|
22
|
+
if (model) args.push('--model', model);
|
|
23
|
+
if (effort) args.push('--effort', effort);
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const env = { ...process.env };
|
|
26
|
+
delete env.CLAUDECODE; // prevent nested-session detection
|
|
27
|
+
const proc = spawn('claude', args, {
|
|
28
|
+
cwd,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
env,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
proc.stdout.on('data', d => { stdout += d; });
|
|
36
|
+
proc.stderr.on('data', d => { stderr += d; });
|
|
37
|
+
|
|
38
|
+
proc.on('close', code => {
|
|
39
|
+
if (code !== 0) {
|
|
40
|
+
reject(new Error(stderr || `claude exited with code ${code}`));
|
|
41
|
+
} else {
|
|
42
|
+
resolve(stdout);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
proc.on('error', err => {
|
|
46
|
+
reject(new Error(`Failed to spawn claude: ${err.message}. Is Claude Code installed?`));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { spawnClaude };
|
package/package.json
CHANGED
|
@@ -1,9 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemmafit",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"description": "Make agents prove that their code is correct.",
|
|
4
|
+
"author": "midspiral",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
6
|
+
"homepage": "https://lemmafit.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/midspiral/lemmafit.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"formal verification",
|
|
13
|
+
"claude",
|
|
14
|
+
"dafny",
|
|
15
|
+
"verified",
|
|
16
|
+
"proof"
|
|
17
|
+
],
|
|
18
|
+
"version": "0.2.0",
|
|
19
|
+
"type": "commonjs",
|
|
20
|
+
"files": [
|
|
21
|
+
"cli/",
|
|
22
|
+
"lib/",
|
|
23
|
+
"docs/",
|
|
24
|
+
"skills/",
|
|
25
|
+
"commands/",
|
|
26
|
+
"kernels/",
|
|
27
|
+
"blank-template/"
|
|
28
|
+
],
|
|
29
|
+
"bin": {
|
|
30
|
+
"lemmafit": "cli/lemmafit.js",
|
|
31
|
+
"lemmafit-daemon": "cli/daemon.js",
|
|
32
|
+
"lemmafit-verify-hook": "cli/verify-hook.js",
|
|
33
|
+
"lemmafit-context-hook": "cli/context-hook.js",
|
|
34
|
+
"lemmafit-download-dafny": "lib/download-dafny.js",
|
|
35
|
+
"lemmafit-sync": "cli/sync.js",
|
|
36
|
+
"lemmafit-session-hook": "cli/session-hook.js",
|
|
37
|
+
"lemmafit-generate-guarantees": "cli/generate-guarantees-md.js"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"postinstall": "node ./cli/sync.js && node ./cli/download-dafny2js.js && node ./lib/download-dafny.js"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"claimcheck": "^0.2.0",
|
|
44
|
+
"js-yaml": "^4.1.1",
|
|
45
|
+
"ws": "^8.18.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
9
53
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-dafny
|
|
3
|
+
description: Dafny code patterns and reference for lemmafit apps. Use when writing or editing .dfy files, defining state machines (Model, Action, Inv, Init, Step), or when Dafny verification fails and you need to fix errors. Covers the AppCore module pattern and common mistakes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lemmafit Dafny
|
|
7
|
+
|
|
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)
|
|
10
|
+
|
|
11
|
+
## Dafny Pattern Example
|
|
12
|
+
|
|
13
|
+
Given the `Replay` kernel, a simple counter app with inherited undo/redo could be written like this
|
|
14
|
+
|
|
15
|
+
```dafny
|
|
16
|
+
include "Replay.dfy"
|
|
17
|
+
|
|
18
|
+
module CounterDomain refines Domain {
|
|
19
|
+
// The model is the state of your application
|
|
20
|
+
type Model = int
|
|
21
|
+
|
|
22
|
+
// Actions are the ways the state can change
|
|
23
|
+
datatype Action = Inc | Dec
|
|
24
|
+
|
|
25
|
+
// Invariant: what must always be true about the state
|
|
26
|
+
predicate Inv(m: Model) {
|
|
27
|
+
m >= 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initial state
|
|
31
|
+
function Init(): Model {
|
|
32
|
+
0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// How actions transform the state
|
|
36
|
+
function Apply(m: Model, a: Action): Model {
|
|
37
|
+
match a
|
|
38
|
+
case Inc => m + 1
|
|
39
|
+
case Dec => m - 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Normalization: fix invalid states (called after Apply)
|
|
43
|
+
function Normalize(m: Model): Model {
|
|
44
|
+
if m < 0 then 0 else m
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Proof that Init satisfies the invariant
|
|
48
|
+
lemma InitSatisfiesInv()
|
|
49
|
+
ensures Inv(Init())
|
|
50
|
+
{
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Proof that every step preserves the invariant
|
|
54
|
+
lemma StepPreservesInv(m: Model, a: Action)
|
|
55
|
+
// requires Inv(m) is inherited and should not be repeated
|
|
56
|
+
ensures Inv(Normalize(Apply(m, a)))
|
|
57
|
+
{
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module CounterKernel refines Kernel {
|
|
62
|
+
import D = CounterDomain
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module AppCore {
|
|
66
|
+
import K = CounterKernel
|
|
67
|
+
import D = CounterDomain
|
|
68
|
+
|
|
69
|
+
function Init(): K.History { K.InitHistory() }
|
|
70
|
+
|
|
71
|
+
function Inc(): D.Action { D.Inc }
|
|
72
|
+
function Dec(): D.Action { D.Dec }
|
|
73
|
+
|
|
74
|
+
function Dispatch(h: K.History, a: D.Action): K.History requires K.HistInv(h) { K.Do(h, a) }
|
|
75
|
+
function Undo(h: K.History): K.History { K.Undo(h) }
|
|
76
|
+
function Redo(h: K.History): K.History { K.Redo(h) }
|
|
77
|
+
|
|
78
|
+
function Present(h: K.History): D.Model { h.present }
|
|
79
|
+
function CanUndo(h: K.History): bool { |h.past| > 0 }
|
|
80
|
+
function CanRedo(h: K.History): bool { |h.future| > 0 }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Common Mistakes to Avoid
|
|
86
|
+
|
|
87
|
+
- It is an error to repeat inherited `requires` clauses.
|
|
88
|
+
- It is OK to have `assume {:axiom} false` in _proofs_, temporarily, as the pieces are put together. Strive for zero such axioms eventually.
|
|
89
|
+
- Nested pattern matching _is_ allowed, but needs to be properly parenthesized. Example (out of context):
|
|
90
|
+
```
|
|
91
|
+
function optimize(e: exp): exp
|
|
92
|
+
{
|
|
93
|
+
match e
|
|
94
|
+
case EInt(v) => e
|
|
95
|
+
case EVar(x) => e
|
|
96
|
+
case EAdd(e1, e2) => (match (optimize(e1), optimize(e2))
|
|
97
|
+
case (EInt(0), e2) => e2
|
|
98
|
+
case (e1, EInt(0)) => e1
|
|
99
|
+
case (e1, e2) => EAdd(e1, e2))
|
|
100
|
+
}
|
|
101
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-post-react-audit
|
|
3
|
+
description: Audit workflow for lemmafit apps after React. Runs after writing React code to catch logic that slipped through. Performs audit and labels findings by severity and iterates until only minor findings remain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lemmafit Post React Audit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Audit: Logic-in-JS-post
|
|
10
|
+
|
|
11
|
+
Check whether any effect-free logic required for the current build phase is found written directly in JavaScript or TypeScript instead of Dafny.
|
|
12
|
+
|
|
13
|
+
### What to check
|
|
14
|
+
|
|
15
|
+
1. **State derivations in React** — Any computed value derived from state (filtering, sorting, calculations, conditional logic) that is not exposed through `Api.Present` or a Dafny function. These belong in Dafny.
|
|
16
|
+
2. **Validation in JS** — Input validation, constraint checks, or boundary enforcement done in React hooks or components instead of Dafny predicates.
|
|
17
|
+
3. **Business rules in event handlers** — Pure conditional guards on dispatching (e.g., "only allow X if Y" where Y is derivable from model state) that aren't enforced by Dafny preconditions or exposed as predicates. Exclude effect-gating logic (e.g., "only call Stripe API if active") — that belongs in JS.
|
|
18
|
+
4. **Formatting with logic** — Display formatting that encodes business rules (e.g., color based on threshold, status text based on state) rather than pure cosmetic formatting.
|
|
19
|
+
5. **Duplicated logic** — Logic that exists in Dafny and is available via API but is re-implemented in JS (even partially), creating a consistency risk.
|
|
20
|
+
6. **Utils with hidden logic** — Utility functions in `src/utils` that contain domain logic rather than pure display helpers.
|
|
21
|
+
|
|
22
|
+
## Severity Labels
|
|
23
|
+
|
|
24
|
+
Label each finding with one of:
|
|
25
|
+
|
|
26
|
+
| Severity | Meaning | Action |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `critical` | Effect-free logic in JS that could produce wrong behavior if it diverges from Dafny, or logic duplicated between JS and Dafny | Must move to Dafny before shipping |
|
|
29
|
+
| `moderate` | Effect-free logic in JS that should be a Dafny function or predicate but isn't yet causing a correctness risk | Should move to Dafny |
|
|
30
|
+
| `minor` | Borderline case where JS logic is arguably pure formatting, or a trivial derivation not worth a Dafny round-trip | Can keep in JS, document why |
|
|
31
|
+
|
|
32
|
+
## Output Format
|
|
33
|
+
|
|
34
|
+
Present findings as a numbered list with severity tag and file location:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## Logic-in-JS Post-React Audit
|
|
38
|
+
|
|
39
|
+
1. [critical] src/hooks/useSubscription.ts:42 — computes prorated refund in JS, duplicates Dafny proration logic
|
|
40
|
+
2. [moderate] src/components/PlanCard.tsx:18 — derives "canUpgrade" from state fields, should be a Dafny predicate
|
|
41
|
+
3. [minor] src/utils/format.ts:10 — date formatting, pure display (OK)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Pass Criteria
|
|
45
|
+
|
|
46
|
+
The audit passes when there are **zero critical and zero moderate findings**. If any remain, move the logic to Dafny (back to Step 2), re-verify, then re-run this audit.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-pre-react-audits
|
|
3
|
+
description: Audit workflow for lemmafit apps before React. Run before writing React code to catch proof gaps and unverified logic. Performs two audits — Proof Strength (are proofs complete and tight?) and Logic-in-JS (is any logic missing from Dafny?). Labels findings by severity and iterates until only minor findings remain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lemmafit Audits
|
|
7
|
+
|
|
8
|
+
Run both audits below in order. Report findings in a structured table. Iterate with the lemmafit-proofs skill (Step 4) until only minor findings remain.
|
|
9
|
+
|
|
10
|
+
## Audit 1: Proof Strength Audit
|
|
11
|
+
|
|
12
|
+
Check the strength of every proof in the Dafny codebase against the corresponding SPEC.yaml entries.
|
|
13
|
+
|
|
14
|
+
### What to check
|
|
15
|
+
|
|
16
|
+
1. **Missing lemmas** — Every `verifiable: true` entry in SPEC.yaml with `type: postcondition` must have a corresponding lemma with a matching `ensures` clause. Invariant-only coverage is not sufficient for postcondition entries.
|
|
17
|
+
2. **Weak postconditions** — Are `ensures` clauses as strong as the property stated in SPEC.yaml? A proof that verifies but proves less than the spec claims is a gap. Look for postconditions that are trivially true or weaker than the stated property.
|
|
18
|
+
3. **Axiom debt** — Flag every `assume {:axiom}` statement. Each one is unverified trust. Check whether a real proof is feasible.
|
|
19
|
+
4. **Invariant tightness** — Every code guard or clamp (e.g., `if x > MAX then MAX else x`) must have a matching bound in `Inv` at full strength (e.g., `0 <= x <= MAX`). Loose invariants hide bugs.
|
|
20
|
+
5. **Missing biconditionals** — For every "if X then Y" lemma, check whether the converse "if not X then not Y" is also proven (or document why it's unnecessary).
|
|
21
|
+
6. **Composition gaps** — If two features interact, is there a lemma proving their combined properties? Implicit composition is a proof gap.
|
|
22
|
+
7. **Normalize reliance** — Check if `StepPreservesInv` only holds because of `Normalize`. If removing `Normalize` would break the proof, flag it — the `Apply` function should ideally preserve the invariant on its own for each action.
|
|
23
|
+
8. **Input validation** — Every trust-boundary datatype should have a `Valid_*` predicate, and `Step` should `requires Valid_*(input)` for external inputs. Missing validation predicates are gaps.
|
|
24
|
+
|
|
25
|
+
## Audit 2: Logic-in-JS
|
|
26
|
+
|
|
27
|
+
Check whether any effect-free logic required for the current build phase is expected to be implemented in JavaScript/TypeScript directly instead of Dafny.
|
|
28
|
+
|
|
29
|
+
### What to check
|
|
30
|
+
|
|
31
|
+
1. **State derivations in React** — Any computed value derived from state (filtering, sorting, calculations, conditional logic) that is not exposed through `Api.Present` or a Dafny function. These belong in Dafny.
|
|
32
|
+
2. **Validation in JS** — Input validation, constraint checks, or boundary enforcement done in React hooks or components instead of Dafny predicates.
|
|
33
|
+
3. **Business rules in event handlers** — Pure conditional guards on dispatching (e.g., "only allow X if Y" where Y is derivable from model state) that aren't enforced by Dafny preconditions or exposed as predicates. Exclude effect-gating logic (e.g., "only call Stripe API if active") — that belongs in JS.
|
|
34
|
+
4. **Formatting with logic** — Display formatting that encodes business rules (e.g., color based on threshold, status text based on state) rather than pure cosmetic formatting.
|
|
35
|
+
5. **Duplicated logic** — Logic that exists in Dafny but is re-implemented in JS (even partially), creating a consistency risk.
|
|
36
|
+
6. **Utils with hidden logic** — Utility functions that contain domain logic rather than pure display helpers.
|
|
37
|
+
|
|
38
|
+
## Severity Labels
|
|
39
|
+
|
|
40
|
+
Label each finding with one of:
|
|
41
|
+
|
|
42
|
+
| Severity | Meaning | Action |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `critical` | Unverified logic that could produce wrong behavior, or a SPEC.yaml postcondition with no corresponding proof | Must fix before proceeding to React |
|
|
45
|
+
| `moderate` | Weak proof that verifies but doesn't fully cover the spec property, or JS logic that should move to Dafny | Should fix before proceeding |
|
|
46
|
+
| `minor` | Style issue, missing biconditional for an edge case, or axiom with clear justification | Can proceed, fix later |
|
|
47
|
+
|
|
48
|
+
## Output Format
|
|
49
|
+
|
|
50
|
+
Present findings as a numbered list grouped by audit, with severity tag:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
## Proof Strength Audit
|
|
54
|
+
|
|
55
|
+
1. [critical] spec-003 "Total weight is sum of sets" — no lemma found, only covered by Inv
|
|
56
|
+
2. [moderate] StepPreservesInv — only holds due to Normalize for AddSet action
|
|
57
|
+
3. [minor] UndoRedoRoundTrip — missing biconditional (converse not meaningful here)
|
|
58
|
+
|
|
59
|
+
## Logic-in-JS Audit
|
|
60
|
+
|
|
61
|
+
1. [critical] src/hooks/useWorkout.ts:24 — filters completed sets using JS logic, should be a Dafny function
|
|
62
|
+
2. [minor] src/utils/format.ts:10 — date formatting, pure display (OK)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Pass Criteria
|
|
66
|
+
|
|
67
|
+
The audit passes when there are **zero critical and zero moderate findings**. If any critical or moderate findings remain, return to Step 4 (proofs) to address them, then re-run audits.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-proofs
|
|
3
|
+
description: Proof-maximizing workflow for writing Dafny lemmas, postconditions, and invariants. Load this skill before writing any lemmas, ensures clauses, or when deciding between axioms and lemmas. Covers postcondition-first development, scaffolding, biconditional completeness, and input validation patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lemmafit Proofs
|
|
7
|
+
|
|
8
|
+
## Proof-Maximizing Workflow
|
|
9
|
+
|
|
10
|
+
1. **Postconditions first** — Write `ensures` clauses before function bodies. The postcondition is the contract; the body is the implementation. This forces you to think about what a function guarantees before how it works.
|
|
11
|
+
|
|
12
|
+
2. **Scaffold before proving** — Create lemma signatures with empty bodies (`lemma Foo() ensures P() {}`) so the Claims panel tracks them as "scaffolded". Fill in proofs afterward. This ensures no claim is forgotten.
|
|
13
|
+
|
|
14
|
+
3. **Biconditional completeness** — For every "if X then Y" claim, also prove "if not X then not Y" (or document why the converse is unnecessary). One-directional implications leave blind spots.
|
|
15
|
+
|
|
16
|
+
4. **Input validation predicates** — Every trust-boundary datatype gets a `Valid_*` predicate (e.g., `predicate Valid_SetWeight(w: int) { w > 0 && w <= 1000 }`). The `Step` function should `requires Valid_*(input)` for external inputs.
|
|
17
|
+
|
|
18
|
+
5. **Invariant tightness** — Every code guard or clamp (e.g., `if x > MAX then MAX else x`) should have a matching bound in `Inv` at full strength (e.g., `0 <= x <= MAX`). Loose invariants hide bugs.
|
|
19
|
+
|
|
20
|
+
6. **Standalone lemmas for compositions** — Don't rely on implicit composition. If feature A and feature B interact, write a lemma proving their combined properties explicitly (e.g., `lemma UndoRedoRoundTrip`).
|
|
21
|
+
|
|
22
|
+
7. **Prefer lemmas over axioms** — `[verified]` = lemma with proof body, `[assumed]` = axiom with justification comment. Every axiom is technical debt. When you add an axiom, add a comment explaining why a proof is infeasible and what would need to change to prove it.
|
|
23
|
+
|
|
24
|
+
8. **Prove application-specific lemmas beyond the general requirements** — For the step `Apply` function, you should prove something specific for each action. Prove strong properties about your program, both generic and domain-specific. *Example*: proving the Replay kernel `StepPreservesInv` (after Normalization) is a weak property. Try to prove that applying more specific actions results in desired properties (e.g. Inv even without Normalization)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-react-pattern
|
|
3
|
+
description: React integration pattern for lemmafit verified apps. Use when building React components that consume the verified Dafny API, wiring up state with Api.Init/Dispatch/Present, or creating new UI for a lemmafit app.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Pattern for Lemmafit
|
|
7
|
+
|
|
8
|
+
- Use the auto-generated TypeScript API from `src/dafny/app.ts` (never edit `app.ts` directly)
|
|
9
|
+
- For any code that touches logic, check if it has been written in Dafny already and is available in the API
|
|
10
|
+
- Do not re-write logic in React that already exists in Dafny/API
|
|
11
|
+
|
|
12
|
+
## Modularity
|
|
13
|
+
|
|
14
|
+
Build modular React apps — never put everything in a single `App.tsx`. Organize `src/` by the same layers used in SPEC.yaml:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/
|
|
18
|
+
├── dafny/app.ts # Auto-generated verified API (DO NOT EDIT)
|
|
19
|
+
├── hooks/ # State layer — custom hooks that wrap the verified API
|
|
20
|
+
│ └── useAppState.ts # Calls Api.Init, Api.Dispatch, Api.Present
|
|
21
|
+
├── components/ # Presentation layer — pure UI components
|
|
22
|
+
│ ├── WorkoutForm.tsx
|
|
23
|
+
│ └── SetList.tsx
|
|
24
|
+
├── utils/ # Utils layer — formatters, parsers, constants
|
|
25
|
+
│ └── format.ts
|
|
26
|
+
├── App.tsx # Root — composes hooks + components, no logic
|
|
27
|
+
└── main.tsx # Entry point
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Layer rules
|
|
31
|
+
|
|
32
|
+
- **Logic** (`dafny/`) — All business logic lives in Dafny. The React side never re-implements or duplicates what the verified API provides.
|
|
33
|
+
- **State** (`hooks/`) — Custom hooks are the *only* place that calls `Api.Init`, `Api.Dispatch`, and `Api.Present`. Components never import from `dafny/` directly.
|
|
34
|
+
- **Presentation** (`components/`) — Pure components that receive data and callbacks via props. No direct API calls, no `useState` for domain state.
|
|
35
|
+
- **Utils** (`utils/`) — Display helpers like formatters and constants. No side effects, no state.
|
|
36
|
+
- **Root** (`App.tsx`) — Wires hooks to components. Should be short — if it's growing, extract a component or hook.
|
|
37
|
+
|
|
38
|
+
### Why this matters
|
|
39
|
+
- Verified logic stays in one place (Dafny) — the React layer is just plumbing
|
|
40
|
+
- Components are testable and reusable without the verified API
|
|
41
|
+
- When the Dafny model changes, only `hooks/` needs updating — components stay stable
|
|
42
|
+
|
|
43
|
+
## Standard Pattern
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import * as Api from './dafny/app';
|
|
47
|
+
|
|
48
|
+
function App() {
|
|
49
|
+
const [state, setState] = useState(() => Api.Init());
|
|
50
|
+
|
|
51
|
+
const inc = () => setState(Api.Dispatch(state, Api.Inc()));
|
|
52
|
+
const value = Api.Present(state);
|
|
53
|
+
|
|
54
|
+
return <button onClick={inc}>{value}</button>;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Key Rules
|
|
59
|
+
- Always initialize state with `Api.Init()`
|
|
60
|
+
- Dispatch actions through `Api.Dispatch(state, action)`
|
|
61
|
+
- Read display values through `Api.Present(state)`
|
|
62
|
+
- Never modify state directly — all transitions go through the verified Step function
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lemmafit-spec
|
|
3
|
+
description: Lemmafit development workflow for managing SPEC.yaml and Dafny specifications. Use when the user describes a new feature, asks to add or update features or specs, or when spec changes need to be addressed. Handles SPEC.yaml entries, Dafny code sync, and spec queue processing. Follow these instructions whenever your workflow touches the spec.yaml file
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lemmafit Spec
|
|
7
|
+
|
|
8
|
+
## Instructions
|
|
9
|
+
1. When the user describes an app, a requirement, or a new feature, add entries to SPEC.yaml first
|
|
10
|
+
2. Then write Dafny specs that formalize entries where `verifiable: true`
|
|
11
|
+
3. Mark UI-only or non-provable entries as `verifiable: false, status: trusted`
|
|
12
|
+
4. Keep SPEC.yaml in sync with Dafny code — if you change one, update the other
|
|
13
|
+
5. When the user edits SPEC.yaml directly, you will be notified via the daemon — update code to match
|
|
14
|
+
|
|
15
|
+
## RULES YOU MUST FOLLOW
|
|
16
|
+
- ALL logic-related and state machine entries must be set as `verifiable: true`
|
|
17
|
+
- ONLY EXCEPTIONS: API calls, parsing, and any other external effect
|
|
18
|
+
|
|
19
|
+
## Checking for Spec Changes
|
|
20
|
+
At the start of every conversation, read `.vibe/status.json`. If `specQueue` has items, these are spec changes that haven't been addressed yet. Each item has a type (`added` or `removed`) and the text. Added items include a line number in the current SPEC.yaml. Update Dafny code and/or React code to reflect these requirements before doing anything else.
|
|
21
|
+
|
|
22
|
+
The queue auto-clears when you write `.dfy` files and verification passes. For trusted-only changes, writing SPEC.yaml with updated tags will also clear the queue.
|
|
23
|
+
|
|
24
|
+
Prove strong properties about your program, both generic and domain-specific.
|
|
25
|
+
**Example**: proving the Replay kernel `StepPreservesInv` (after Normalization) is a weak property. Try to prove that applying more specific actions results in desired properties (e.g. Inv even without Normalization)
|
|
26
|
+
|
|
27
|
+
Every SPEC.yaml entry with type: postcondition and verifiable: true MUST have a corresponding lemma in Dafny with an ensures clause that matches the property field. Invariant-only proofs are NOT sufficient for postcondition entries.
|
|
28
|
+
|
|
29
|
+
### Format
|
|
30
|
+
SPEC.yaml is a structured YAML file with an `entries` list. Each entry has:
|
|
31
|
+
- `id` — unique spec ID (e.g. `spec-001`)
|
|
32
|
+
- `req_id` — linked requirement ID (or `null`)
|
|
33
|
+
- `title` — human-readable description of the property
|
|
34
|
+
- `group` — logical grouping (e.g. Business Logic, Presentation, Data, Utils)
|
|
35
|
+
- `layer` — architecture layer (`logic`, `presentation`, `state`, `data`, `utils`)
|
|
36
|
+
- `type` — property type (`invariant`, `postcondition`, `precondition`, `datatype`, `function`, `constraint`)
|
|
37
|
+
- `property` — formal property expression. **MUST be quoted** if it contains special characters (`:`, `>`, `!`, `#`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `?`, `|`, `-`, `=`). When in doubt, always quote with double quotes.
|
|
38
|
+
- `module` — target Dafny module (or `null` for non-verifiable)
|
|
39
|
+
- `depends_on` — list of spec IDs this entry depends on
|
|
40
|
+
- `verifiable` — whether this can be proven in Dafny
|
|
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`
|
|
43
|
+
|
|
44
|
+
### Example
|
|
45
|
+
```yaml
|
|
46
|
+
entries:
|
|
47
|
+
- id: spec-001
|
|
48
|
+
req_id: null
|
|
49
|
+
title: The counter value is always non-negative
|
|
50
|
+
group: Business Logic
|
|
51
|
+
layer: logic
|
|
52
|
+
type: invariant
|
|
53
|
+
property: "model.value >= 0"
|
|
54
|
+
module: AppCore
|
|
55
|
+
depends_on: []
|
|
56
|
+
verifiable: true
|
|
57
|
+
status: verified
|
|
58
|
+
- id: spec-002
|
|
59
|
+
req_id: null
|
|
60
|
+
title: The increment button displays the current count
|
|
61
|
+
group: Presentation
|
|
62
|
+
layer: presentation
|
|
63
|
+
type: invariant
|
|
64
|
+
property: "display == Present(state).value"
|
|
65
|
+
module: null
|
|
66
|
+
depends_on: []
|
|
67
|
+
verifiable: false
|
|
68
|
+
guarantee_type: trusted
|
|
69
|
+
state: null
|
|
70
|
+
```
|
|
71
|
+
|