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,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Downloads prebuilt dafny2js 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 DAFNY2JS_VERSION = '0.9.5';
|
|
14
|
+
|
|
15
|
+
const PLATFORM_RIDS = {
|
|
16
|
+
'darwin-arm64': 'osx-arm64',
|
|
17
|
+
'darwin-x64': 'osx-x64',
|
|
18
|
+
'linux-x64': 'linux-x64',
|
|
19
|
+
'linux-arm64': 'linux-arm64',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getPlatformKey() {
|
|
23
|
+
return `${os.platform()}-${os.arch()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function guessRid(platformKey) {
|
|
27
|
+
return PLATFORM_RIDS[platformKey] || (platformKey === 'win32-x64' ? 'win-x64' : platformKey);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printBuildInstructions(rid) {
|
|
31
|
+
const installDir = path.join(os.homedir(), '.lemmafit', '.dafny2js');
|
|
32
|
+
console.error('');
|
|
33
|
+
console.error('To build from source (requires .NET 8 SDK):');
|
|
34
|
+
console.error(' git clone https://github.com/metareflection/dafny2js.git');
|
|
35
|
+
console.error(' cd dafny2js');
|
|
36
|
+
console.error(` dotnet publish -c Release -r ${rid} --self-contained /p:PublishSingleFile=true`);
|
|
37
|
+
console.error('');
|
|
38
|
+
console.error('Then copy the binary to:');
|
|
39
|
+
console.error(` ${path.join(installDir, 'dafny2js')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function download(url, dest) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const request = (url) => {
|
|
45
|
+
https.get(url, (response) => {
|
|
46
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
47
|
+
request(response.headers.location);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (response.statusCode !== 200) {
|
|
52
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const total = parseInt(response.headers['content-length'], 10);
|
|
57
|
+
let downloaded = 0;
|
|
58
|
+
|
|
59
|
+
response.on('data', (chunk) => {
|
|
60
|
+
downloaded += chunk.length;
|
|
61
|
+
const pct = total ? Math.round((downloaded / total) * 100) : '?';
|
|
62
|
+
process.stdout.write(`\rDownloading dafny2js... ${pct}%`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const file = fs.createWriteStream(dest);
|
|
66
|
+
response.pipe(file);
|
|
67
|
+
|
|
68
|
+
file.on('finish', () => {
|
|
69
|
+
file.close();
|
|
70
|
+
console.log(' Done.');
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
}).on('error', (err) => {
|
|
74
|
+
fs.unlink(dest, () => {});
|
|
75
|
+
reject(err);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
request(url);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const platformKey = getPlatformKey();
|
|
85
|
+
const rid = PLATFORM_RIDS[platformKey];
|
|
86
|
+
|
|
87
|
+
if (!rid) {
|
|
88
|
+
console.error(`No prebuilt dafny2js binary available for: ${platformKey}`);
|
|
89
|
+
console.error('Prebuilt binaries are available for:', Object.keys(PLATFORM_RIDS).join(', '));
|
|
90
|
+
printBuildInstructions(guessRid(platformKey));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const installDir = path.join(os.homedir(), '.lemmafit', '.dafny2js');
|
|
95
|
+
const binaryPath = path.join(installDir, 'dafny2js');
|
|
96
|
+
const versionFile = path.join(installDir, 'version');
|
|
97
|
+
|
|
98
|
+
// Check if correct version is already installed
|
|
99
|
+
if (fs.existsSync(binaryPath) && fs.existsSync(versionFile)) {
|
|
100
|
+
const installed = fs.readFileSync(versionFile, 'utf8').trim();
|
|
101
|
+
if (installed === DAFNY2JS_VERSION) {
|
|
102
|
+
console.log(`dafny2js v${DAFNY2JS_VERSION} already installed.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`dafny2js ${installed} -> ${DAFNY2JS_VERSION}, upgrading...`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const asset = `dafny2js-${rid}.tar.gz`;
|
|
111
|
+
const url = `https://github.com/metareflection/dafny2js/releases/download/v${DAFNY2JS_VERSION}/${asset}`;
|
|
112
|
+
const tarPath = path.join(installDir, asset);
|
|
113
|
+
|
|
114
|
+
console.log(`Downloading dafny2js v${DAFNY2JS_VERSION} for ${platformKey}...`);
|
|
115
|
+
await download(url, tarPath);
|
|
116
|
+
|
|
117
|
+
console.log('Extracting...');
|
|
118
|
+
execSync(`tar xzf "${tarPath}" -C "${installDir}"`, { stdio: 'inherit' });
|
|
119
|
+
|
|
120
|
+
fs.unlinkSync(tarPath);
|
|
121
|
+
|
|
122
|
+
if (os.platform() !== 'win32') {
|
|
123
|
+
fs.chmodSync(binaryPath, '755');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(versionFile, DAFNY2JS_VERSION);
|
|
127
|
+
console.log(`dafny2js v${DAFNY2JS_VERSION} installed to ${installDir}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
console.error('Failed to download dafny2js:', err.message);
|
|
132
|
+
console.error('');
|
|
133
|
+
console.error('You can download it manually from:');
|
|
134
|
+
console.error(' https://github.com/metareflection/dafny2js/releases');
|
|
135
|
+
printBuildInstructions(guessRid(getPlatformKey()));
|
|
136
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generates lemmafit/.vibe/guarantees.md deterministically from:
|
|
4
|
+
* - lemmafit/.vibe/guarantees.json (claim-to-spec mapping)
|
|
5
|
+
* - lemmafit/.vibe/claimcheck.json (claimcheck results, optional)
|
|
6
|
+
* - SPEC.yaml (for trusted entries and group info)
|
|
7
|
+
*
|
|
8
|
+
* Usage: node cli/generate-guarantees-md.js [projectDir]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
|
|
15
|
+
function run(projectDir) {
|
|
16
|
+
const absDir = path.resolve(projectDir || '.');
|
|
17
|
+
const vibePath = path.join(absDir, 'lemmafit', '.vibe');
|
|
18
|
+
|
|
19
|
+
// --- Load guarantees.json ---
|
|
20
|
+
const guaranteesPath = path.join(vibePath, 'guarantees.json');
|
|
21
|
+
if (!fs.existsSync(guaranteesPath)) {
|
|
22
|
+
console.error('Error: guarantees.json not found. Run /guarantees first.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const data = JSON.parse(fs.readFileSync(guaranteesPath, 'utf8'));
|
|
26
|
+
|
|
27
|
+
// --- Load claimcheck results (optional) ---
|
|
28
|
+
let claimcheckResults = [];
|
|
29
|
+
const claimcheckPath = path.join(vibePath, 'claimcheck.json');
|
|
30
|
+
if (fs.existsSync(claimcheckPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(claimcheckPath, 'utf8').trim();
|
|
33
|
+
if (raw) {
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
// Handle both array and object formats
|
|
36
|
+
if (Array.isArray(parsed)) {
|
|
37
|
+
claimcheckResults = parsed;
|
|
38
|
+
} else if (parsed && typeof parsed === 'object') {
|
|
39
|
+
claimcheckResults = parsed.results || parsed.claims || [parsed];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Load SPEC.yaml for trusted entries and groups ---
|
|
46
|
+
const specPath = path.join(absDir, 'SPEC.yaml');
|
|
47
|
+
let specEntries = [];
|
|
48
|
+
if (fs.existsSync(specPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
51
|
+
specEntries = (parsed && parsed.entries) || [];
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`Warning: Failed to parse SPEC.yaml: ${e.reason || e.message}`);
|
|
54
|
+
console.error('Hint: property values with special characters (::, ==>, !in) must be quoted in YAML.');
|
|
55
|
+
console.error('Continuing without SPEC.yaml data (trusted entries and groups will be missing).');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const trustedEntries = specEntries.filter(e => !e.verifiable || e.status === 'trusted');
|
|
60
|
+
const verifiableEntries = specEntries.filter(e => e.verifiable);
|
|
61
|
+
|
|
62
|
+
// --- Build spec lookup ---
|
|
63
|
+
const specById = {};
|
|
64
|
+
for (const e of specEntries) specById[e.id] = e;
|
|
65
|
+
|
|
66
|
+
// --- Compute coverage ---
|
|
67
|
+
const guarantees = data.guarantees || [];
|
|
68
|
+
const gaps = data.gaps || [];
|
|
69
|
+
const coveredIds = new Set(guarantees.map(g => g.specId));
|
|
70
|
+
const verifiedCount = coveredIds.size;
|
|
71
|
+
const verifiableTotal = verifiableEntries.length;
|
|
72
|
+
|
|
73
|
+
// --- Collect axioms from guarantees ---
|
|
74
|
+
const axioms = [];
|
|
75
|
+
for (const g of guarantees) {
|
|
76
|
+
for (const c of (g.coveredBy || [])) {
|
|
77
|
+
if (c.type === 'axiom') axioms.push(c);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Build claimcheck lookup ---
|
|
82
|
+
const claimcheckByLemma = {};
|
|
83
|
+
for (const r of claimcheckResults) {
|
|
84
|
+
claimcheckByLemma[r.lemmaName] = r;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Group guarantees by SPEC.yaml group ---
|
|
88
|
+
const groups = {};
|
|
89
|
+
for (const g of guarantees) {
|
|
90
|
+
const spec = specById[g.specId];
|
|
91
|
+
const groupName = (spec && spec.group) || 'Other';
|
|
92
|
+
if (!groups[groupName]) groups[groupName] = [];
|
|
93
|
+
groups[groupName].push({ ...g, spec });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sort groups by first spec ID in each group
|
|
97
|
+
const sortedGroupNames = Object.keys(groups).sort((a, b) => {
|
|
98
|
+
const aFirst = groups[a][0].specId || '';
|
|
99
|
+
const bFirst = groups[b][0].specId || '';
|
|
100
|
+
return aFirst.localeCompare(bFirst);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Sort entries within each group by spec ID
|
|
104
|
+
for (const name of sortedGroupNames) {
|
|
105
|
+
groups[name].sort((a, b) => (a.specId || '').localeCompare(b.specId || ''));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Build claimcheck table rows (only entries with lemma mappings) ---
|
|
109
|
+
const claimcheckRows = [];
|
|
110
|
+
for (const g of guarantees) {
|
|
111
|
+
for (const c of (g.coveredBy || [])) {
|
|
112
|
+
if (!c.lemmaName) continue;
|
|
113
|
+
const result = claimcheckByLemma[c.lemmaName];
|
|
114
|
+
const status = result ? (result.status || result.result || 'Unknown') : 'Not checked';
|
|
115
|
+
if (!claimcheckRows.find(r => r.lemmaName === c.lemmaName)) {
|
|
116
|
+
claimcheckRows.push({
|
|
117
|
+
requirement: g.requirement,
|
|
118
|
+
lemmaName: c.lemmaName,
|
|
119
|
+
status: capitalize(status)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Generate markdown ---
|
|
126
|
+
const lines = [];
|
|
127
|
+
const date = new Date().toISOString().split('T')[0];
|
|
128
|
+
|
|
129
|
+
lines.push('# Guarantees Report');
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push(`Generated: ${date}`);
|
|
132
|
+
lines.push('');
|
|
133
|
+
|
|
134
|
+
// Coverage
|
|
135
|
+
lines.push('## Coverage');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(`- **${verifiedCount}/${verifiableTotal}** verifiable spec entries covered`);
|
|
138
|
+
lines.push(`- **${gaps.length} gaps**`);
|
|
139
|
+
lines.push(`- **${axioms.length} axioms** (${axioms.length === 0 ? 'zero trust surface' : 'trust surface below'})`);
|
|
140
|
+
lines.push(`- **${trustedEntries.length} trusted entries** (presentation/runtime, not verifiable in Dafny)`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
// Claimcheck results table
|
|
144
|
+
if (claimcheckRows.length > 0) {
|
|
145
|
+
lines.push('## Claimcheck Results');
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push('| Requirement | Lemma | Status |');
|
|
148
|
+
lines.push('|------------|-------|--------|');
|
|
149
|
+
for (const row of claimcheckRows) {
|
|
150
|
+
lines.push(`| ${row.requirement} | \`${row.lemmaName}\` | ${row.status} |`);
|
|
151
|
+
}
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Proven guarantees grouped
|
|
156
|
+
lines.push('## Proven Guarantees');
|
|
157
|
+
lines.push('');
|
|
158
|
+
for (const groupName of sortedGroupNames) {
|
|
159
|
+
const items = groups[groupName];
|
|
160
|
+
const firstId = items[0].specId;
|
|
161
|
+
const lastId = items[items.length - 1].specId;
|
|
162
|
+
const range = firstId === lastId ? firstId : `${firstId} to ${lastId}`;
|
|
163
|
+
lines.push(`### ${groupName} (${range})`);
|
|
164
|
+
|
|
165
|
+
for (const item of items) {
|
|
166
|
+
const lemmaNames = (item.coveredBy || [])
|
|
167
|
+
.filter(c => c.lemmaName)
|
|
168
|
+
.map(c => c.lemmaName);
|
|
169
|
+
const suffix = lemmaNames.length > 0 ? ` — \`${lemmaNames.join('`, `')}\`` : '';
|
|
170
|
+
lines.push(`- ${item.requirement}${suffix}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Gaps
|
|
176
|
+
if (gaps.length > 0) {
|
|
177
|
+
lines.push('## Gaps');
|
|
178
|
+
lines.push('');
|
|
179
|
+
for (const gap of gaps) {
|
|
180
|
+
lines.push(`- **${gap.specId}**: ${gap.requirement} — ${gap.reason}`);
|
|
181
|
+
}
|
|
182
|
+
lines.push('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Trust surface
|
|
186
|
+
lines.push('## Trust Surface');
|
|
187
|
+
lines.push('');
|
|
188
|
+
if (axioms.length === 0) {
|
|
189
|
+
lines.push('**Axioms: 0** — All properties are fully proven.');
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(`**Axioms: ${axioms.length}**`);
|
|
192
|
+
lines.push('');
|
|
193
|
+
for (const a of axioms) {
|
|
194
|
+
lines.push(`- \`${a.file || ''}:${a.line || ''}\`: ${a.content || a.expression || ''}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
if (trustedEntries.length > 0) {
|
|
199
|
+
lines.push('**Trusted entries** (not verifiable in Dafny):');
|
|
200
|
+
for (const e of trustedEntries) {
|
|
201
|
+
lines.push(`- ${e.id}: ${e.title}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Write output ---
|
|
207
|
+
const output = lines.join('\n');
|
|
208
|
+
const reportsDir = path.join(absDir, 'lemmafit', 'reports');
|
|
209
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
210
|
+
const outPath = path.join(reportsDir, 'guarantees.md');
|
|
211
|
+
fs.writeFileSync(outPath, output);
|
|
212
|
+
console.log(`Wrote ${outPath}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function capitalize(s) {
|
|
216
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { run };
|
|
220
|
+
|
|
221
|
+
if (require.main === module) {
|
|
222
|
+
run(process.argv[2]);
|
|
223
|
+
}
|