@mikeargento/bitgraph-mcp 0.1.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.
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
17
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
18
- import { buildServer } from "../server.js";
18
+ import { buildServer, type FusedSummary } from "../server.js";
19
19
  import { toUrlSafeB64 } from "../encoding.js";
20
20
 
21
21
  interface Recorded {
@@ -32,7 +32,6 @@ let fileB = "";
32
32
  let fileC = "";
33
33
  let digestA = ""; // standard b64 of fileA bytes
34
34
  let digestB = "";
35
- let commitMode: "ok" | "429" | "short" = "ok";
36
35
  const EPOCH = createHash("sha256").update("test-epoch").digest("base64");
37
36
  let mintCounter = 100;
38
37
 
@@ -90,17 +89,8 @@ before(async () => {
90
89
  }
91
90
  send(200, { results });
92
91
  } else if (url.pathname === "/api/commit") {
93
- if (commitMode === "429") {
94
- res.writeHead(429, { "Content-Type": "application/json", "Retry-After": "30" });
95
- res.end(JSON.stringify({ error: "Rate limit exceeded: per-client proof rate limit exceeded" }));
96
- return;
97
- }
98
- const digests = (body as { digests: Array<{ digestB64: string }> }).digests;
99
- if (commitMode === "short") {
100
- send(200, digests.slice(1).map((d) => proofFor(d.digestB64)));
101
- return;
102
- }
103
- send(200, digests.map((d) => proofFor(d.digestB64)));
92
+ // Ordinary recording is no longer what the record tool does; nothing here should call it.
93
+ send(500, { error: "unexpected /api/commit" });
104
94
  } else if (url.pathname.startsWith("/api/proofs/digest/")) {
105
95
  const digest = decodeURIComponent(url.pathname.split("/").pop() ?? "");
106
96
  if (digest === toUrlSafeB64(digestA)) {
@@ -143,8 +133,24 @@ after(() => {
143
133
  delete process.env["BITGRAPH_API_KEY"];
144
134
  });
145
135
 
136
+ /** A stand-in for the fuse pipeline: no slot, no enclave; records what it was asked to fuse. */
137
+ const fusedCalls: Array<{ name: string; digestB64: string }> = [];
138
+ let fuseMode: "ok" | "fail" = "ok";
139
+ async function fakeFuse(bytes: Uint8Array, name: string): Promise<FusedSummary> {
140
+ const digestB64 = createHash("sha256").update(bytes).digest("base64");
141
+ fusedCalls.push({ name, digestB64 });
142
+ if (fuseMode === "fail") throw new Error("the boundary is restarting");
143
+ const artifactDigestB64 = createHash("sha256").update("fused:" + digestB64).digest("base64");
144
+ return {
145
+ proof: proofFor(artifactDigestB64) as unknown as FusedSummary["proof"],
146
+ frame: { type: "bitgraph-fuse/1" },
147
+ placement: "container/1",
148
+ artifactDigestB64,
149
+ originDigestB64: digestB64,
150
+ };
151
+ }
146
152
  async function connectedClient(): Promise<Client> {
147
- const server = buildServer();
153
+ const server = buildServer({ fuseFile: fakeFuse });
148
154
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
149
155
  const client = new Client({ name: "test-client", version: "0.0.0" });
150
156
  await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
@@ -158,56 +164,53 @@ test("lists the three tools", async () => {
158
164
  assert.deepEqual(names, ["bitgraph_check", "bitgraph_get_proof", "bitgraph_record"]);
159
165
  });
160
166
 
161
- test("record dedups on-record files and mints only fresh ones", async () => {
167
+ test("record dedups on-record files and fuses only fresh ones", async () => {
162
168
  const client = await connectedClient();
163
169
  requests.length = 0;
170
+ fusedCalls.length = 0;
164
171
  const result = await client.callTool({
165
172
  name: "bitgraph_record",
166
173
  arguments: { paths: [fileA, fileB] },
167
174
  });
168
175
  assert.ok(!result.isError, JSON.stringify(result.content));
169
-
170
- const commit = requests.find((r) => r.path === "/api/commit");
171
- if (!commit) throw new Error("no commit request was sent");
172
- const commitBody = commit.body as { digests: Array<{ digestB64: string; hashAlg: string }>; chainId: string };
173
- assert.equal(commitBody.chainId, "bitgraph:main");
174
- assert.deepEqual(commitBody.digests, [{ digestB64: digestB, hashAlg: "sha256" }], "only fileB minted, standard b64");
175
- assert.equal(commit.headers["authorization"], "Bearer test-key-123", "api key forwarded");
176
-
176
+ assert.equal(requests.filter((r) => r.path === "/api/commit").length, 0, "nothing is committed as bytes-only");
177
+ assert.deepEqual(fusedCalls.map((c) => c.digestB64), [digestB], "only fileB fused; fileA was on record");
178
+ assert.equal(fusedCalls[0]?.name, "b.txt", "the artifact is named after the file");
177
179
  const batch = requests.find((r) => r.path === "/api/proofs/batch");
178
- if (!batch) throw new Error("no batch check preceded the commit");
180
+ if (!batch) throw new Error("no batch check preceded the fuse");
179
181
  const batchBody = batch.body as { digests: string[] };
180
182
  assert.ok(batchBody.digests.every((d) => !d.includes("+") && !d.includes("=")), "check uses url-safe digests");
181
-
182
183
  const structured = result.structuredContent as {
183
- results: Array<{ path: string; outcome: string; counter: string | null }>;
184
- summary: { recorded: number; on_record: number };
184
+ results: Array<{ path: string; outcome: string; counter: string | null; placement: string | null; artifact_digest: string | null }>;
185
+ frames: Record<string, unknown>;
186
+ summary: { fused: number; on_record: number; not_fused: number };
185
187
  };
186
- assert.equal(structured.summary.recorded, 1);
188
+ assert.equal(structured.summary.fused, 1);
187
189
  assert.equal(structured.summary.on_record, 1);
190
+ assert.equal(structured.summary.not_fused, 0);
188
191
  const a = structured.results.find((r) => r.path === fileA);
189
192
  const b = structured.results.find((r) => r.path === fileB);
190
193
  assert.equal(a?.outcome, "on record");
191
194
  assert.equal(a?.counter, "10", "on-record outcome reports the EARLIEST position");
192
- assert.equal(b?.outcome, "recorded");
195
+ assert.equal(b?.outcome, "fused");
196
+ assert.equal(b?.placement, "container/1");
197
+ assert.ok(b?.artifact_digest && structured.frames[b.artifact_digest], "the Frame rides in the structured result under the fused digest");
193
198
  });
194
199
 
195
- test("record with again=true mints even on-record digests", async () => {
200
+ test("record with again=true fuses even on-record files", async () => {
196
201
  const client = await connectedClient();
197
- requests.length = 0;
202
+ fusedCalls.length = 0;
198
203
  const result = await client.callTool({
199
204
  name: "bitgraph_record",
200
205
  arguments: { paths: [fileA], again: true },
201
206
  });
202
207
  assert.ok(!result.isError, JSON.stringify(result.content));
203
- const commit = requests.find((r) => r.path === "/api/commit");
204
- const commitBody = commit?.body as { digests: Array<{ digestB64: string }> };
205
- assert.deepEqual(commitBody.digests.map((d) => d.digestB64), [digestA]);
208
+ assert.deepEqual(fusedCalls.map((c) => c.digestB64), [digestA]);
206
209
  const structured = result.structuredContent as {
207
210
  results: Array<{ outcome: string; total_positions: number }>;
208
211
  };
209
- assert.equal(structured.results[0]?.outcome, "recorded");
210
- assert.equal(structured.results[0]?.total_positions, 3, "two prior positions plus the new one");
212
+ assert.equal(structured.results[0]?.outcome, "fused");
213
+ assert.equal(structured.results[0]?.total_positions, 3, "two prior positions plus the new artifact");
211
214
  });
212
215
 
213
216
  test("check reports positions without ever committing", async () => {
@@ -274,49 +277,29 @@ test("get_proof requires exactly one selector", async () => {
274
277
  assert.ok(result.isError);
275
278
  });
276
279
 
277
- test("commit failure labels unminted files as 'not recorded', never 'on record'", async () => {
280
+ test("a fuse failure labels the file 'not fused', never 'on record'", async () => {
278
281
  const client = await connectedClient();
279
- commitMode = "429";
282
+ fuseMode = "fail";
280
283
  try {
281
284
  const result = await client.callTool({
282
285
  name: "bitgraph_record",
283
286
  arguments: { paths: [fileC] },
284
287
  });
285
- assert.ok(result.isError, "partial failure must be an error result");
288
+ assert.ok(result.isError, "a failure must be an error result");
286
289
  const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
287
- assert.ok(text.includes("Retry after 30 seconds"), text);
288
290
  assert.ok(text.includes("0 of 1"), text);
291
+ assert.ok(text.includes("the boundary is restarting"), text);
289
292
  const structured = result.structuredContent as {
290
- results: Array<{ outcome: string; proof_url: string | null; counter: string | null }>;
291
- summary: { recorded: number; on_record: number; not_recorded: number };
293
+ results: Array<{ outcome: string; proof_url: string | null; error?: string }>;
294
+ summary: { fused: number; on_record: number; not_fused: number };
292
295
  };
293
- assert.equal(structured.results[0]?.outcome, "not recorded");
296
+ assert.equal(structured.results[0]?.outcome, "not fused");
294
297
  assert.equal(structured.results[0]?.proof_url, null);
295
- assert.equal(structured.summary.not_recorded, 1);
296
- assert.equal(structured.summary.recorded, 0);
298
+ assert.equal(structured.summary.not_fused, 1);
299
+ assert.equal(structured.summary.fused, 0);
297
300
  assert.equal(structured.summary.on_record, 0);
298
301
  } finally {
299
- commitMode = "ok";
300
- }
301
- });
302
-
303
- test("a 200 with fewer proofs than digests is treated as a partial failure", async () => {
304
- const client = await connectedClient();
305
- commitMode = "short";
306
- try {
307
- const result = await client.callTool({
308
- name: "bitgraph_record",
309
- arguments: { paths: [fileC] },
310
- });
311
- assert.ok(result.isError, "shortfall must not reach the success path");
312
- const structured = result.structuredContent as {
313
- results: Array<{ outcome: string }>;
314
- };
315
- assert.equal(structured.results[0]?.outcome, "not recorded");
316
- const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
317
- assert.ok(text.includes("fewer proofs than digests"), text);
318
- } finally {
319
- commitMode = "ok";
302
+ fuseMode = "ok";
320
303
  }
321
304
  });
322
305
 
@@ -330,5 +313,5 @@ test("record surfaces unreadable paths before any network call", async () => {
330
313
  assert.ok(result.isError);
331
314
  assert.equal(requests.length, 0, "no API call happened; nothing was minted");
332
315
  const text = (result.content as Array<{ text: string }>)[0]?.text ?? "";
333
- assert.ok(text.includes("nothing was recorded"));
316
+ assert.ok(text.includes("nothing was BitGraphed"));
334
317
  });
package/src/format.ts CHANGED
@@ -31,18 +31,25 @@ export function proofUrl(
31
31
  }
32
32
 
33
33
  /**
34
- * One recording outcome, in the product's own vocabulary. "not recorded" is
35
- * the honest label for a file lost to a partial commit failure: never claim
36
- * "on record" for a digest that has no proof.
34
+ * One outcome per path, in the product's own vocabulary. "fused": a new fused
35
+ * artifact was built from the file and committed under its own slot. "on
36
+ * record": the bytes already had a recording or a fused artifact naming them
37
+ * as origin, and nothing was minted. "not fused": the attempt failed; never
38
+ * claim "on record" for bytes that have no proof.
37
39
  */
38
40
  export interface RecordOutcome {
39
41
  path: string;
40
- digest: string; // URL-safe
41
- outcome: "recorded" | "on record" | "not recorded";
42
+ /** The file's own digest (URL-safe): the origin of the fused artifact. */
43
+ digest: string;
44
+ outcome: "fused" | "on record" | "not fused";
45
+ /** The fused artifact's digest (URL-safe), present on a "fused" outcome. */
46
+ artifact_digest: string | null;
47
+ placement: string | null;
42
48
  counter: string | null;
43
49
  epoch: string | null; // URL-safe
44
50
  total_positions: number;
45
51
  proof_url: string | null;
52
+ error?: string;
46
53
  }
47
54
 
48
55
  export interface CheckOutcome {
@@ -60,33 +67,38 @@ export function positionOf(proof: BitGraphProof): { counter: string | null; epoc
60
67
  }
61
68
 
62
69
  export function renderRecordMarkdown(outcomes: readonly RecordOutcome[]): string {
63
- const recorded = outcomes.filter((o) => o.outcome === "recorded");
70
+ const fused = outcomes.filter((o) => o.outcome === "fused");
64
71
  const onRecord = outcomes.filter((o) => o.outcome === "on record");
65
- const notRecorded = outcomes.filter((o) => o.outcome === "not recorded");
72
+ const notFused = outcomes.filter((o) => o.outcome === "not fused");
66
73
  const lines: string[] = [];
67
- let headline = `${recorded.length} recorded, ${onRecord.length} already on record.`;
68
- if (notRecorded.length > 0) {
69
- headline = `${recorded.length} recorded, ${onRecord.length} already on record, ${notRecorded.length} NOT recorded.`;
74
+ let headline = `${fused.length} fused, ${onRecord.length} already on record.`;
75
+ if (notFused.length > 0) {
76
+ headline = `${fused.length} fused, ${onRecord.length} already on record, ${notFused.length} NOT fused.`;
70
77
  }
71
78
  lines.push(headline);
72
79
  for (const o of outcomes) {
73
- if (o.outcome === "not recorded") {
74
- lines.push(`- not recorded (commit failed) · ${o.path}`);
80
+ if (o.outcome === "not fused") {
81
+ lines.push(`- not fused · ${o.path}${o.error ? `: ${o.error}` : ""}`);
75
82
  continue;
76
83
  }
77
- const positionNote =
84
+ const note =
78
85
  o.outcome === "on record"
79
86
  ? o.total_positions > 1
80
- ? ` (${o.total_positions} causal positions, earliest shown)`
87
+ ? ` (${o.total_positions} positions, earliest shown)`
81
88
  : ""
82
- : "";
89
+ : o.placement
90
+ ? ` (${o.placement})`
91
+ : "";
92
+ lines.push(`- ${o.outcome} · #${o.counter ?? "?"} · ${o.path}${note}\n ${o.proof_url}`);
93
+ }
94
+ if (fused.length > 0) {
83
95
  lines.push(
84
- `- ${o.outcome} · #${o.counter ?? "?"} · ${o.path}${positionNote}\n ${o.proof_url}`
96
+ "\nEach fused artifact was built in memory from the file, hashed and committed under its own slot; the file itself is unchanged and was not uploaded. The original plus the proof rebuilds the fused bytes; the Frame for each is in the structured result."
85
97
  );
86
98
  }
87
99
  if (onRecord.length > 0) {
88
100
  lines.push(
89
- `\nAlready-recorded files were not re-recorded. To record one of them at a new causal position deliberately, call bitgraph_record with again=true.`
101
+ "\nFiles already on record were left alone. To make a new fused artifact from one of them deliberately, call bitgraph_record with again=true."
90
102
  );
91
103
  }
92
104
  return lines.join("\n");