@skillstech/thunderlang 0.2.0 → 0.4.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/src/mcp.mjs CHANGED
@@ -6,19 +6,79 @@
6
6
  //
7
7
  // Start it with `thunder mcp`. Point an MCP client at that command.
8
8
 
9
+ import { execSync } from 'node:child_process';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { basename, dirname, join } from 'node:path';
9
12
  import { parseIntent } from './parse.mjs';
10
- import { semanticDiagnostics, COMPILER_VERSION } from './emit.mjs';
13
+ import { semanticDiagnostics, buildProof, COMPILER_VERSION } from './emit.mjs';
14
+ import { sha256 } from './hash.mjs';
11
15
  import { verifyDiff } from './verify-diff.mjs';
12
16
  import { liftSource, SUPPORTED_LANGUAGES } from './lift.mjs';
13
17
  import { runTests } from './testing.mjs';
14
18
  import { evaluateDecision } from './runtime.mjs';
15
19
  import { buildIntentGraph } from './intent-graph.mjs';
20
+ import { buildConformance } from './conformance.mjs';
21
+ import { checkDrift } from './drift.mjs';
16
22
  import { draftIntent } from './draft.mjs';
17
23
  import { ALL_DIAGNOSTICS } from './intent-schema.mjs';
18
24
 
19
25
  const PROTOCOL_VERSION = '2024-11-05';
20
26
  const str = { type: 'string' };
21
27
 
28
+ // ── prove helpers ────────────────────────────────────────────────────────────
29
+ // Per-claim resolution , mirrors resolveObligations in cli.mjs (shared there by `thunder test
30
+ // --contracts` and `thunder prove`) so the MCP proof and the CLI proof agree claim for claim.
31
+ // States: verified (proven by a passing named test), failed (its test fails), declared (a
32
+ // verification is named but not runnable in-file), unverified (nothing).
33
+ function resolveObligations(ast) {
34
+ const t = runTests(ast);
35
+ const testPass = new Map();
36
+ for (const res of (t.results || [])) {
37
+ const cur = testPass.get(res.target);
38
+ testPass.set(res.target, cur === undefined ? !!res.pass : cur && !!res.pass);
39
+ }
40
+ const norm = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
41
+ const matchTest = (verifyText) => {
42
+ const v = norm(verifyText); if (!v) return null;
43
+ for (const [name, pass] of testPass) { const n = norm(name); if (n && (n === v || n.includes(v) || v.includes(n))) return { name, pass }; }
44
+ return null;
45
+ };
46
+ const build = (kind, o) => {
47
+ const verify = o.verify || [];
48
+ let status = 'unverified', provenBy = null;
49
+ if (verify.length) {
50
+ let m = null;
51
+ for (const vt of verify) { const x = matchTest(vt); if (x) { m = x; if (!x.pass) break; } }
52
+ if (m) { status = m.pass ? 'verified' : 'failed'; provenBy = m.name; } else status = 'declared';
53
+ }
54
+ return { kind, id: o.id, status, provenBy };
55
+ };
56
+ const obligations = [...ast.guarantees.map((g) => build('guarantee', g)), ...ast.neverRules.map((n) => build('prohibition', n))];
57
+ return { obligations, tests: t, failed: obligations.filter((o) => o.status === 'failed').length };
58
+ }
59
+
60
+ // Freshness tuple , the (intent, implementation, dependencies, compiler, environment) binding
61
+ // `thunder verify` recomputes later to mark a proof STALE. Mirrors freshnessFor in cli.mjs;
62
+ // implementation/dependencies are read from the server's working directory when available.
63
+ const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'poetry.lock', 'Pipfile.lock', 'Cargo.lock', 'go.sum', 'Gemfile.lock', 'composer.lock'];
64
+ function proofFreshness(proof, environment) {
65
+ let head = null;
66
+ try { head = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim() || null; } catch { head = null; }
67
+ let lock = null, dir = process.cwd();
68
+ for (let i = 0; i < 6 && !lock; i++) {
69
+ for (const lf of LOCKFILES) { const p = join(dir, lf); if (existsSync(p)) { lock = p; break; } }
70
+ const parent = dirname(dir); if (parent === dir) break; dir = parent;
71
+ }
72
+ return {
73
+ intentHash: proof.sourceHash,
74
+ compilerVersion: proof.compilerVersion,
75
+ implementation: head,
76
+ dependencies: lock ? { file: basename(lock), hash: sha256(readFileSync(lock, 'utf8')) } : null,
77
+ environment: environment || null,
78
+ generatedAt: proof.generatedAt,
79
+ };
80
+ }
81
+
22
82
  // Each tool is a pure function of its arguments. `run` returns a JSON-able value or a string.
23
83
  const TOOLS = [
24
84
  {
@@ -44,6 +104,70 @@ const TOOLS = [
44
104
  },
45
105
  run: ({ intent, after, before = null, language = 'typescript' }) => verifyDiff(String(intent), { before: before == null ? null : String(before), after: String(after), language }),
46
106
  },
107
+ {
108
+ name: 'intent_prove',
109
+ description: 'Emit the durable intent-proof-v1 artifact for .intent source: per-claim verdicts (verified / failed / planned / needs_verification, each bound to the named test that proves it) plus the freshness tuple (intent hash, compiler version, commit, dependency lockfile) that lets `thunder verify` mark the proof STALE later. Mirrors `thunder prove`; an unverified claim never reads as passed. Call it after the gate passes to record WHAT was proven.',
110
+ inputSchema: {
111
+ type: 'object', required: ['source'],
112
+ properties: {
113
+ source: { ...str, description: 'the .intent source' },
114
+ sourceFile: { ...str, description: 'file name to record in the proof (default intent.thunder)' },
115
+ environment: { ...str, description: 'environment name to bind into the freshness tuple (optional)' },
116
+ },
117
+ },
118
+ run: ({ source, sourceFile = 'intent.thunder', environment }) => {
119
+ const text = String(source);
120
+ const ast = parseIntent(text);
121
+ const diagnostics = semanticDiagnostics(ast);
122
+ const resolved = resolveObligations(ast);
123
+ const proof = buildProof(ast, {
124
+ sourceFile: String(sourceFile),
125
+ sourceHash: sha256(text),
126
+ targetsRequested: ast.targets || [],
127
+ targetsGenerated: [],
128
+ diagnostics,
129
+ generatedAt: new Date().toISOString(),
130
+ });
131
+ // Fold per-claim verdicts into the proof so it records real status, not just planned/needs_verification.
132
+ const CLAIM_MAP = { verified: 'verified', failed: 'failed', declared: 'planned', unverified: 'needs_verification' };
133
+ const byId = new Map(resolved.obligations.map((o) => [o.id, o]));
134
+ const applyStatus = (claim) => { const o = byId.get(claim.id); return o ? { ...claim, status: CLAIM_MAP[o.status], provenBy: o.provenBy || null } : claim; };
135
+ proof.guarantees = proof.guarantees.map(applyStatus);
136
+ proof.neverRules = proof.neverRules.map(applyStatus);
137
+ proof.freshness = proofFreshness(proof, environment);
138
+ const ok = proof.verification.semanticPassed && resolved.tests.ok !== false && resolved.failed === 0;
139
+ return { ok, proofId: `proof-${proof.sourceHash.replace('sha256:', '').slice(0, 6)}`, ...proof, tests: resolved.tests };
140
+ },
141
+ },
142
+ {
143
+ name: 'intent_conform',
144
+ description: 'Grade cross-target conformance (thunder-conformance-v1): the deterministic engine defines the canonical result every in-file test case must produce, and each target\'s outputs (passed via `results`) are graded against it. Same tests, every implementation , a diverging target case is a CONFORMANCE FAILURE. Mirrors `thunder conform`. Without `results`, targets honestly stay "declared", never "pass".',
145
+ inputSchema: {
146
+ type: 'object', required: ['source'],
147
+ properties: {
148
+ source: { ...str, description: 'the .intent source (with test blocks)' },
149
+ targets: { type: 'array', items: str, description: 'target languages to grade (default: the targets the intent declares)' },
150
+ results: { type: 'object', description: 'per-target actual outputs to grade: {target: {"Test / case": result}}' },
151
+ },
152
+ },
153
+ run: ({ source, targets = [], results = null }) => {
154
+ const rep = buildConformance(parseIntent(String(source)), { targets: Array.isArray(targets) ? targets.map(String) : [], results });
155
+ return { ok: rep.semanticFailures === 0 && rep.failures.length === 0, ...rep };
156
+ },
157
+ },
158
+ {
159
+ name: 'intent_drift',
160
+ description: 'Check whether real code, as it exists TODAY, still satisfies its intent , the standing-guard complement to intent_verify_diff (no diff needed). Re-lifts the code and reports drift findings: guarantees with no matching evidence, never-rules with no guard, declared inputs missing from the signature, and new behavior the intent never declared. Mirrors `thunder drift`.',
161
+ inputSchema: {
162
+ type: 'object', required: ['intent', 'code'],
163
+ properties: {
164
+ intent: { ...str, description: 'the approved .intent source (the contract)' },
165
+ code: { ...str, description: 'the implementation source as it exists now' },
166
+ language: { ...str, description: `source language (default typescript). One of: ${SUPPORTED_LANGUAGES.join(', ')}` },
167
+ },
168
+ },
169
+ run: ({ intent, code, language = 'typescript' }) => checkDrift(String(intent), String(code), { language }),
170
+ },
47
171
  {
48
172
  name: 'intent_draft',
49
173
  description: 'Turn a STRUCTURED brief into a rigorous, canonically-formatted ThunderLang draft plus a review checklist of what a human must still fill in (unverified guarantees, unguarded secrets, missing goal). Use this after distilling a user request into structured fields; the draft is a proposal for human approval, never verified.',