@optitransfer/proofline 0.0.0-stage → 0.2.0-next.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/AGENTS.md +32 -0
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +29 -0
- package/LICENSE +154 -0
- package/NOTICE +10 -0
- package/PATENTS-NONASSERT.md +1 -0
- package/README.md +192 -2
- package/bin/proofline.mjs +97 -0
- package/docs/00-index.md +43 -0
- package/docs/01-scope.md +70 -0
- package/docs/02-requirements.md +64 -0
- package/docs/03-domain-model.md +43 -0
- package/docs/04-architecture.md +79 -0
- package/docs/05-lifecycle.md +47 -0
- package/docs/06-verification.md +57 -0
- package/docs/07-evidence.md +69 -0
- package/docs/08-adapters.md +61 -0
- package/docs/09-security.md +64 -0
- package/docs/10-conformance.md +59 -0
- package/docs/11-development-pipeline.md +50 -0
- package/docs/12-roadmap.md +104 -0
- package/docs/13-interdependencies.md +53 -0
- package/docs/14-metrics.md +40 -0
- package/docs/adr/0001-product-boundary.md +28 -0
- package/docs/adr/0002-evidence-and-gates.md +22 -0
- package/docs/adr/0003-standalone-first.md +18 -0
- package/package.json +57 -4
- package/src/cache.mjs +81 -0
- package/src/checks.mjs +271 -0
- package/src/claims.mjs +60 -0
- package/src/contract.mjs +163 -0
- package/src/engine.mjs +329 -0
- package/src/evidence.mjs +45 -0
- package/src/forbidden.mjs +57 -0
- package/src/index.mjs +203 -0
- package/src/mcp.mjs +55 -0
- package/src/util.mjs +92 -0
package/src/mcp.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Minimal MCP server (JSON-RPC 2.0 over newline-delimited stdio). Zero dependencies.
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import * as P from './index.mjs';
|
|
4
|
+
|
|
5
|
+
const BASE = { type: 'string', description: 'The revision the task started from. Needed in a fresh clone unless task_next pinned it or HEAD carries a Proofline-Base: trailer.' };
|
|
6
|
+
const FRESH = { type: 'boolean', description: 'Run every check again, ignoring results reused under defaults.reuse_results.' };
|
|
7
|
+
const OWNER = { type: 'string', description: "Only this owner's tasks and unowned ones; never another owner's." };
|
|
8
|
+
const TOOLS = [
|
|
9
|
+
{ name: 'contract_create', description: 'Add a task contract (id, title, phase, depends_on, scope, criteria with deterministic verifiers). Returns validity problems; a task without verifiers cannot pass.', inputSchema: { type: 'object', properties: { task: { type: 'object' } }, required: ['task'] } },
|
|
10
|
+
{ name: 'task_next', description: 'Return the single next task (phase- and dependency-gated) with its full brief. Call this before starting any work.', inputSchema: { type: 'object', properties: { owner: OWNER } } },
|
|
11
|
+
{ name: 'evidence_record', description: 'Record agent-side evidence (notes, commands you ran). Stored hash-chained, but never used to pass a gate: verify re-runs everything itself.', inputSchema: { type: 'object', properties: { type: { type: 'string' }, data: { type: 'object' } }, required: ['type'] } },
|
|
12
|
+
{ name: 'verify', description: 'Independently run every acceptance check for a task, plus forbidden-action, scope, wiring and claim checks. Returns decision and a corrective prompt.', inputSchema: { type: 'object', properties: { task: { type: 'string' }, claims: { type: 'string', description: 'Your status line(s): TASK-ID | status | commit | tests | measured vs target | missing' }, base: BASE, fresh: FRESH }, required: ['task'] } },
|
|
13
|
+
{ name: 'gate', description: 'Release decision PASS/FAIL/BLOCKED with Markdown+JSON evidence report.', inputSchema: { type: 'object', properties: { task: { type: 'string' }, claims: { type: 'string' }, base: BASE, fresh: FRESH }, required: ['task'] } },
|
|
14
|
+
{ name: 'report', description: 'Latest gate report for a task (Markdown).', inputSchema: { type: 'object', properties: { task: { type: 'string' } }, required: ['task'] } },
|
|
15
|
+
{ name: 'status', description: 'Task and phase status, evidence-chain integrity.', inputSchema: { type: 'object', properties: { owner: OWNER } } },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function handle(root, msg) {
|
|
19
|
+
const { id, method, params = {} } = msg;
|
|
20
|
+
const ok = (result) => ({ jsonrpc: '2.0', id, result });
|
|
21
|
+
if (method === 'initialize') return ok({ protocolVersion: params.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'proofline', version: '0.1.0' } });
|
|
22
|
+
if (method === 'ping') return ok({});
|
|
23
|
+
if (method === 'tools/list') return ok({ tools: TOOLS });
|
|
24
|
+
if (method === 'tools/call') {
|
|
25
|
+
const a = params.arguments || {};
|
|
26
|
+
try {
|
|
27
|
+
let r;
|
|
28
|
+
switch (params.name) {
|
|
29
|
+
case 'contract_create': r = P.contractCreate(root, a.task); break;
|
|
30
|
+
case 'task_next': r = P.taskNext(root, { owner: a.owner }); break;
|
|
31
|
+
case 'evidence_record': r = P.evidenceRecord(root, a.type, a.data || {}); break;
|
|
32
|
+
case 'verify': { const g = P.E.verifyTask(root, a.task, { claims: a.claims, base: a.base, fresh: a.fresh === true }); r = { ...g, corrective_prompt: P.E.steer(g, P.C.load(root)) }; break; }
|
|
33
|
+
case 'gate': r = P.E.gate(root, a.task, { claims: a.claims, base: a.base, fresh: a.fresh === true }); break;
|
|
34
|
+
case 'report': r = P.report(root, a.task); break;
|
|
35
|
+
case 'status': r = P.status(root, { owner: a.owner }); break;
|
|
36
|
+
default: return { jsonrpc: '2.0', id, error: { code: -32601, message: `unknown tool ${params.name}` } };
|
|
37
|
+
}
|
|
38
|
+
const text = r.brief ? r.brief + '\n\n' + JSON.stringify({ task: r.task, phase: r.phase }) : r.corrective_prompt ? `DECISION: ${r.decision}\n\n${r.corrective_prompt}\n\n${JSON.stringify({ decision: r.decision, failing: r.failing, blocked: r.blocked, measured: r.measured })}` : r.markdown || JSON.stringify(r, null, 2);
|
|
39
|
+
return ok({ content: [{ type: 'text', text }], isError: false });
|
|
40
|
+
} catch (e) { return ok({ content: [{ type: 'text', text: `error: ${e.message}` }], isError: true }); }
|
|
41
|
+
}
|
|
42
|
+
if (id === undefined) return null; // notification
|
|
43
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `method not found: ${method}` } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serve(root) {
|
|
47
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
48
|
+
rl.on('line', (line) => {
|
|
49
|
+
if (!line.trim()) return;
|
|
50
|
+
let msg; try { msg = JSON.parse(line); } catch { process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }) + '\n'); return; }
|
|
51
|
+
const r = handle(root, msg);
|
|
52
|
+
if (r) process.stdout.write(JSON.stringify(r) + '\n');
|
|
53
|
+
});
|
|
54
|
+
return new Promise((res) => rl.on('close', res));
|
|
55
|
+
}
|
package/src/util.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
7
|
+
|
|
8
|
+
/** Owner marker for these exact bytes. The directory comes from `init --approvals`. A symlink is not a regular file. */
|
|
9
|
+
export function approvalMarker(root, bytes) {
|
|
10
|
+
let dir = null;
|
|
11
|
+
try {
|
|
12
|
+
const c = JSON.parse(fs.readFileSync(path.join(root, '.proofline', 'config.json'), 'utf8'));
|
|
13
|
+
if (typeof c.approvals === 'string' && c.approvals.trim()) dir = c.approvals;
|
|
14
|
+
} catch { dir = null; }
|
|
15
|
+
if (!bytes) return { hex: null, marker: null, approved: false };
|
|
16
|
+
const hex = sha256(bytes);
|
|
17
|
+
const marker = dir ? path.join(dir, hex) : null;
|
|
18
|
+
let approved = false;
|
|
19
|
+
if (marker) { try { approved = fs.lstatSync(marker).isFile(); } catch { approved = false; } }
|
|
20
|
+
return { hex, marker, approved };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Stable JSON (sorted keys) so hashes are reproducible. */
|
|
24
|
+
export function canon(v) {
|
|
25
|
+
if (Array.isArray(v)) return '[' + v.map(canon).join(',') + ']';
|
|
26
|
+
if (v && typeof v === 'object') return '{' + Object.keys(v).sort().filter((k) => v[k] !== undefined).map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',') + '}';
|
|
27
|
+
return JSON.stringify(v ?? null);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Run a shell command; never throws. */
|
|
31
|
+
export function run(cmd, { cwd, timeoutS = 600, env } = {}) {
|
|
32
|
+
const t = performance.now();
|
|
33
|
+
// NODE_TEST_CONTEXT leaks from an enclosing `node --test` and makes a nested `node --test` exit 0 regardless of failures.
|
|
34
|
+
const e = { ...process.env, ...env }; delete e.NODE_TEST_CONTEXT;
|
|
35
|
+
// A pre-push hook exports GIT_DIR. A test or command that inits its own repository must not use it.
|
|
36
|
+
for (const k of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_PREFIX', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_NAMESPACE', 'GIT_QUARANTINE_PATH']) delete e[k];
|
|
37
|
+
const r = spawnSync('bash', ['-c', cmd], { cwd, encoding: 'utf8', timeout: timeoutS * 1000, env: e, maxBuffer: 64 << 20 });
|
|
38
|
+
const ms = Math.round(performance.now() - t);
|
|
39
|
+
const timedOut = r.error?.code === 'ETIMEDOUT';
|
|
40
|
+
return { cmd, cwd, exit: timedOut ? 124 : (r.status ?? 1), stdout: r.stdout || '', stderr: (r.stderr || '') + (r.error && !timedOut ? String(r.error) : ''), ms, timedOut };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function git(args, cwd) {
|
|
44
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 << 20 });
|
|
45
|
+
return { ok: r.status === 0, out: (r.stdout || '').trim(), err: (r.stderr || '').trim() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where commit `sha` stands against <remote>/<branch>. { ok: false, err } when the remote cannot be read; otherwise
|
|
49
|
+
* { ok: true, tip, at, contains, behind, unknown }. A tip this repository lacks is fetched by the branch's name with no ref
|
|
50
|
+
* written (--refmap= keeps the remote-tracking ref, --no-write-fetch-head keeps FETCH_HEAD), so a commit under the tip is
|
|
51
|
+
* found without a fetch beforehand. `unknown`: the tip could not be fetched, so whether it holds the commit is not known. */
|
|
52
|
+
export function onRemote(root, sha, remote, branch) {
|
|
53
|
+
const ls = git(['ls-remote', remote, `refs/heads/${branch}`], root);
|
|
54
|
+
if (!ls.ok) return { ok: false, err: ls.err || `git ls-remote ${remote} failed` };
|
|
55
|
+
const tip = ls.out.split(/\s/)[0] || '';
|
|
56
|
+
if (!tip) return { ok: true, tip: '', at: false, contains: false, behind: null, unknown: false };
|
|
57
|
+
if (tip === sha) return { ok: true, tip, at: true, contains: true, behind: 0, unknown: false };
|
|
58
|
+
const have = () => git(['cat-file', '-e', `${tip}^{commit}`], root).ok;
|
|
59
|
+
if (!have()) git(['fetch', '-q', '--no-tags', '--refmap=', '--no-write-fetch-head', remote, `refs/heads/${branch}`], root);
|
|
60
|
+
if (!have()) return { ok: true, tip, at: false, contains: false, behind: null, unknown: true };
|
|
61
|
+
const contains = git(['merge-base', '--is-ancestor', sha, tip], root).ok;
|
|
62
|
+
return { ok: true, tip, at: false, contains, behind: contains ? Number(git(['rev-list', '--count', `${sha}..${tip}`], root).out) : null, unknown: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const tail = (s, n = 1500) => (s.length > n ? '…' + s.slice(-n) : s);
|
|
66
|
+
|
|
67
|
+
/** Minimal glob → RegExp: ** , * , ? */
|
|
68
|
+
export function globRe(g) {
|
|
69
|
+
let re = '';
|
|
70
|
+
for (let i = 0; i < g.length; i++) {
|
|
71
|
+
const c = g[i];
|
|
72
|
+
if (c === '*' && g[i + 1] === '*') { re += '.*'; i++; if (g[i + 1] === '/') i++; }
|
|
73
|
+
else if (c === '*') re += '[^/]*';
|
|
74
|
+
else if (c === '?') re += '[^/]';
|
|
75
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
76
|
+
}
|
|
77
|
+
return new RegExp('^' + re + '$');
|
|
78
|
+
}
|
|
79
|
+
export const matchAny = (p, globs) => globs.some((g) => globRe(g).test(p));
|
|
80
|
+
|
|
81
|
+
/** Read a dotted path like "latency.p50" from an object. */
|
|
82
|
+
export const pick = (o, p) => p.split('.').reduce((a, k) => (a == null ? undefined : a[k]), o);
|
|
83
|
+
|
|
84
|
+
export function readJson(f, d) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } }
|
|
85
|
+
export function writeJson(f, v) { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, JSON.stringify(v, null, 2) + '\n'); }
|
|
86
|
+
|
|
87
|
+
/** Last JSON object found in a text (bench scripts print JSON as the last line). */
|
|
88
|
+
export function lastJson(text) {
|
|
89
|
+
const lines = text.trim().split('\n').reverse();
|
|
90
|
+
for (const l of lines) { const s = l.trim(); if (s.startsWith('{')) { try { return JSON.parse(s); } catch {} } }
|
|
91
|
+
try { return JSON.parse(text); } catch { return undefined; }
|
|
92
|
+
}
|