c64bridge 0.5.1 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +17 -5
  2. package/data/assembly/assembly-spec.md +24 -0
  3. package/data/assembly/examples/video/hello_world.asm +39 -0
  4. package/data/basic/basic-pitfalls.md +236 -0
  5. package/data/basic/basic-spec.md +507 -198
  6. package/data/video/petscii-style-guide.md +105 -0
  7. package/data/video/sprite-charset-best-practices.md +292 -0
  8. package/dist/c64Client.js +1 -0
  9. package/dist/prompts/registry.js +10 -2
  10. package/dist/rag/knowledgeIndex.js +111 -24
  11. package/dist/rag/retriever.js +110 -4
  12. package/dist/tools/audio.js +26 -13
  13. package/dist/tools/developer.js +34 -0
  14. package/dist/tools/graphics.js +6 -5
  15. package/dist/tools/memory.js +1 -1
  16. package/dist/tools/pollValidator.js +256 -0
  17. package/dist/tools/programRunners.js +352 -12
  18. package/dist/tools/rag.js +1 -7
  19. package/doc/MCP_SETUP.md +7 -1
  20. package/doc/bun/baseline-metrics.md +16 -0
  21. package/doc/bun/migration-checklist.md +55 -0
  22. package/doc/bun/migration.md +83 -0
  23. package/doc/developer.md +72 -10
  24. package/doc/improve-knowledge/notes.md +29 -0
  25. package/doc/improve-knowledge/plan.md +28 -3
  26. package/doc/improve-knowledge/rollout.md +39 -5
  27. package/doc/meta-tools/README.md +101 -0
  28. package/doc/meta-tools/analysis.md +255 -0
  29. package/doc/{meta-tools.md → meta-tools/catalog.md} +2 -2
  30. package/doc/meta-tools/notes.md +174 -0
  31. package/doc/meta-tools/rollout.md +343 -0
  32. package/mcp.json +1 -1
  33. package/package.json +17 -10
  34. package/scripts/c64-cli.mjs +42 -7
  35. package/scripts/c64-tool.sh +7 -1
  36. package/scripts/check-node-compat.mjs +107 -0
  37. package/scripts/invoke-bun.mjs +58 -0
  38. package/scripts/rag-rebuild.mjs +11 -13
  39. package/scripts/run-mcp-check.sh +10 -2
  40. package/scripts/run-tests.mjs +3 -3
  41. package/scripts/run-tests.ts +187 -0
  42. package/scripts/start.mjs +3 -26
  43. package/scripts/update-readme.ts +17 -1
  44. package/scripts/verify-package.mjs +0 -1
  45. package/data/assembly/examples/effects/sinewave.asm +0 -5
  46. package/doc/meta-tools-rollout.md +0 -211
  47. package/scripts/register-ts-node.mjs +0 -6
  48. /package/data/assembly/examples/{graphics/sprite/mover.asm → video/sprite_mover.asm} +0 -0
@@ -1,8 +1,10 @@
1
- import { AssemblyError } from "../assemblyConverter.js";
1
+ import { assemblyToPrg, AssemblyError } from "../assemblyConverter.js";
2
+ import { basicToPrg } from "../basicConverter.js";
2
3
  import { defineToolModule } from "./types.js";
3
4
  import { objectSchema, stringSchema } from "./schema.js";
4
5
  import { textResult } from "./responses.js";
5
6
  import { ToolExecutionError, ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
7
+ import { pollForProgramOutcome } from "./pollValidator.js";
6
8
  function extractFailureDetails(details) {
7
9
  if (details === undefined || details === null) {
8
10
  return undefined;
@@ -21,6 +23,161 @@ function toRecord(details) {
21
23
  }
22
24
  return { value: details };
23
25
  }
26
+ const BASIC_MAX_LINE = 63999;
27
+ function parseBasicRuntimeErrors(screen) {
28
+ const errors = [];
29
+ const seen = new Set();
30
+ const rows = screen.replace(/\r\n?/g, "\n").split("\n");
31
+ for (const row of rows) {
32
+ if (!row || !row.toUpperCase().includes("ERROR IN")) {
33
+ continue;
34
+ }
35
+ const match = /ERROR\s+IN\s+(\d{1,5})/i.exec(row);
36
+ if (!match) {
37
+ continue;
38
+ }
39
+ const lineNumber = Number.parseInt(match[1] ?? "", 10);
40
+ if (!Number.isInteger(lineNumber) || lineNumber < 1 || lineNumber > BASIC_MAX_LINE) {
41
+ continue;
42
+ }
43
+ if (seen.has(lineNumber)) {
44
+ continue;
45
+ }
46
+ seen.add(lineNumber);
47
+ const typeMatch = /\?([A-Z ?]+?)\s+ERROR\s+IN/i.exec(row.toUpperCase());
48
+ const type = typeMatch?.[1]?.trim().replace(/\s+/g, " ") || undefined;
49
+ errors.push({
50
+ line: lineNumber,
51
+ type,
52
+ raw: row.trim(),
53
+ });
54
+ }
55
+ return errors;
56
+ }
57
+ function attemptAutoFixBasicProgram(program, errors) {
58
+ if (errors.length === 0) {
59
+ return undefined;
60
+ }
61
+ const normalized = program.replace(/\r\n?/g, "\n");
62
+ const lines = normalized.split("\n");
63
+ const indexByLine = new Map();
64
+ for (let index = 0; index < lines.length; index += 1) {
65
+ const rawLine = lines[index];
66
+ if (!rawLine) {
67
+ continue;
68
+ }
69
+ const match = /^\s*(\d+)\s*(.*)$/u.exec(rawLine);
70
+ if (!match) {
71
+ continue;
72
+ }
73
+ const lineNumber = Number.parseInt(match[1] ?? "", 10);
74
+ if (!Number.isInteger(lineNumber)) {
75
+ continue;
76
+ }
77
+ if (!indexByLine.has(lineNumber)) {
78
+ indexByLine.set(lineNumber, index);
79
+ }
80
+ }
81
+ const changes = [];
82
+ for (const error of errors) {
83
+ const index = indexByLine.get(error.line);
84
+ if (index === undefined) {
85
+ continue;
86
+ }
87
+ const rawLine = lines[index] ?? "";
88
+ const match = /^\s*(\d+)\s*(.*)$/u.exec(rawLine);
89
+ if (!match) {
90
+ continue;
91
+ }
92
+ const lineNumber = Number.parseInt(match[1] ?? "", 10);
93
+ let content = match[2] ?? "";
94
+ const notes = [];
95
+ const quoteAdjusted = content.replace(/""/g, "");
96
+ const quoteCount = (quoteAdjusted.match(/"/g) ?? []).length;
97
+ if (quoteCount % 2 !== 0) {
98
+ content = `${content}"`;
99
+ notes.push('appended missing closing quote (")');
100
+ }
101
+ const sanitized = stripRemarks(stripStrings(content));
102
+ const openParens = (sanitized.match(/\(/g) ?? []).length;
103
+ const closeParens = (sanitized.match(/\)/g) ?? []).length;
104
+ if (openParens > closeParens) {
105
+ const deficit = openParens - closeParens;
106
+ content = `${content}${")".repeat(deficit)}`;
107
+ notes.push(`appended ${deficit} closing parenthesis${deficit > 1 ? "es" : ""}`);
108
+ }
109
+ if (notes.length > 0) {
110
+ const updated = content.length > 0 ? `${lineNumber} ${content}` : `${lineNumber}`;
111
+ lines[index] = updated;
112
+ changes.push({
113
+ line: lineNumber,
114
+ notes,
115
+ });
116
+ }
117
+ }
118
+ if (changes.length === 0) {
119
+ return undefined;
120
+ }
121
+ return {
122
+ program: lines.join("\n"),
123
+ changes,
124
+ };
125
+ }
126
+ function stripStrings(content) {
127
+ let result = "";
128
+ let inString = false;
129
+ for (let index = 0; index < content.length; index += 1) {
130
+ const char = content[index];
131
+ if (char === '"') {
132
+ if (inString && content[index + 1] === '"') {
133
+ index += 1;
134
+ continue;
135
+ }
136
+ inString = !inString;
137
+ continue;
138
+ }
139
+ if (!inString) {
140
+ result += char;
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ function stripRemarks(content) {
146
+ const upper = content.toUpperCase();
147
+ const remIndex = upper.search(/(^|:)\s*REM\b/);
148
+ if (remIndex >= 0) {
149
+ return content.slice(0, remIndex);
150
+ }
151
+ return content;
152
+ }
153
+ function normalizeRuntimeErrors(errors) {
154
+ return errors.map((error) => ({
155
+ line: error.line,
156
+ ...(error.type ? { type: error.type } : {}),
157
+ text: error.raw,
158
+ }));
159
+ }
160
+ function structuredExecutionError(message, data) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: message,
166
+ },
167
+ ],
168
+ metadata: {
169
+ error: {
170
+ kind: "execution",
171
+ details: data,
172
+ },
173
+ },
174
+ structuredContent: {
175
+ type: "json",
176
+ data,
177
+ },
178
+ isError: true,
179
+ };
180
+ }
24
181
  const uploadBasicArgsSchema = objectSchema({
25
182
  description: "Arguments for uploading and running a BASIC program.",
26
183
  properties: {
@@ -85,7 +242,7 @@ export const programRunnersModule = defineToolModule({
85
242
  description: "Upload a BASIC program to the C64 and execute it immediately. Refer to c64://specs/basic for syntax and device I/O.",
86
243
  summary: "Uploads Commodore BASIC v2 source and runs it via Ultimate 64 firmware.",
87
244
  inputSchema: uploadBasicArgsSchema.jsonSchema,
88
- relatedResources: ["c64://specs/basic", "c64://context/bootstrap"],
245
+ relatedResources: ["c64://specs/basic", "c64://docs/basic/pitfalls", "c64://context/bootstrap"],
89
246
  relatedPrompts: ["basic-program"],
90
247
  tags: ["basic", "execution"],
91
248
  prerequisites: ["read_screen"],
@@ -105,17 +262,143 @@ export const programRunnersModule = defineToolModule({
105
262
  try {
106
263
  const parsed = uploadBasicArgsSchema.parse(args);
107
264
  ctx.logger.info("Uploading BASIC program", { sourceLength: parsed.program.length });
108
- const result = await ctx.client.uploadAndRunBasic(parsed.program);
265
+ const originalProgram = parsed.program;
266
+ // Compute PRG locally to expose structured metadata
267
+ let activeProgram = originalProgram;
268
+ let prg = basicToPrg(activeProgram);
269
+ let entryAddress = prg.readUInt16LE(0);
270
+ const runBasic = async (source) => ctx.client.uploadAndRunBasic(source);
271
+ let result = await runBasic(activeProgram);
109
272
  if (!result.success) {
110
273
  throw new ToolExecutionError("C64 firmware reported failure while running BASIC program", {
111
274
  details: extractFailureDetails(result.details),
112
275
  });
113
276
  }
114
- const base = textResult("BASIC program uploaded and executed successfully.", {
277
+ let screenOutput;
278
+ try {
279
+ screenOutput = await ctx.client.readScreen();
280
+ }
281
+ catch (screenError) {
282
+ ctx.logger.warn("Unable to read screen after BASIC execution", toRecord(screenError));
283
+ }
284
+ let autoFixInfo;
285
+ if (screenOutput) {
286
+ const errors = parseBasicRuntimeErrors(screenOutput);
287
+ if (errors.length > 0) {
288
+ ctx.logger.warn("Detected BASIC runtime errors", {
289
+ errors: errors.map((error) => ({ line: error.line, type: error.type })),
290
+ });
291
+ const normalizedErrors = normalizeRuntimeErrors(errors);
292
+ const fixAttempt = attemptAutoFixBasicProgram(activeProgram, errors);
293
+ if (!fixAttempt) {
294
+ const data = {
295
+ kind: "basic_runtime_error",
296
+ programSource: originalProgram,
297
+ errors: normalizedErrors,
298
+ ...(screenOutput ? { screen: screenOutput } : {}),
299
+ autoFix: {
300
+ attempted: false,
301
+ },
302
+ };
303
+ return structuredExecutionError("Detected BASIC runtime errors after execution.", data);
304
+ }
305
+ ctx.logger.info("Attempting BASIC auto-fix", {
306
+ changes: fixAttempt.changes.map((change) => ({
307
+ line: change.line,
308
+ notes: change.notes,
309
+ })),
310
+ });
311
+ const retryResult = await ctx.client.uploadAndRunBasic(fixAttempt.program);
312
+ if (!retryResult.success) {
313
+ const data = {
314
+ kind: "basic_runtime_error",
315
+ programSource: originalProgram,
316
+ errors: normalizedErrors,
317
+ ...(screenOutput ? { screen: screenOutput } : {}),
318
+ autoFix: {
319
+ attempted: true,
320
+ changes: fixAttempt.changes,
321
+ programSource: fixAttempt.program,
322
+ failure: {
323
+ reason: "firmware_failure",
324
+ details: extractFailureDetails(retryResult.details),
325
+ },
326
+ },
327
+ };
328
+ return structuredExecutionError("Auto-fix failed due to firmware error while re-running BASIC program.", data);
329
+ }
330
+ let retryScreen;
331
+ try {
332
+ retryScreen = await ctx.client.readScreen();
333
+ }
334
+ catch (retryScreenError) {
335
+ ctx.logger.warn("Unable to read screen after BASIC auto-fix execution", toRecord(retryScreenError));
336
+ }
337
+ const remainingErrors = retryScreen ? parseBasicRuntimeErrors(retryScreen) : [];
338
+ if (remainingErrors.length > 0) {
339
+ const data = {
340
+ kind: "basic_runtime_error",
341
+ programSource: originalProgram,
342
+ errors: normalizedErrors,
343
+ ...(screenOutput ? { screen: screenOutput } : {}),
344
+ autoFix: {
345
+ attempted: true,
346
+ changes: fixAttempt.changes,
347
+ programSource: fixAttempt.program,
348
+ resultingErrors: normalizeRuntimeErrors(remainingErrors),
349
+ ...(retryScreen ? { screen: retryScreen } : {}),
350
+ },
351
+ };
352
+ return structuredExecutionError("BASIC program still reports errors after auto-fix attempt.", data);
353
+ }
354
+ activeProgram = fixAttempt.program;
355
+ prg = basicToPrg(activeProgram);
356
+ entryAddress = prg.readUInt16LE(0);
357
+ result = retryResult;
358
+ screenOutput = retryScreen;
359
+ autoFixInfo = {
360
+ changes: fixAttempt.changes,
361
+ originalErrors: errors,
362
+ };
363
+ }
364
+ }
365
+ const message = autoFixInfo
366
+ ? "Detected BASIC errors on execution; applied auto-fix and re-ran successfully."
367
+ : "BASIC program uploaded and executed successfully.";
368
+ const metadata = {
115
369
  success: true,
370
+ entryAddress,
371
+ prgSize: prg.length,
116
372
  details: result.details ?? null,
117
- });
118
- return base;
373
+ ...(screenOutput ? { screen: screenOutput } : {}),
374
+ ...(autoFixInfo
375
+ ? {
376
+ autoFix: {
377
+ applied: true,
378
+ changes: autoFixInfo.changes,
379
+ originalErrors: autoFixInfo.originalErrors,
380
+ },
381
+ }
382
+ : {}),
383
+ };
384
+ const data = {
385
+ kind: "upload_and_run_basic",
386
+ format: "prg",
387
+ entryAddress,
388
+ prgSize: prg.length,
389
+ resources: ["c64://specs/basic", "c64://context/bootstrap"],
390
+ ...(screenOutput ? { screen: screenOutput } : {}),
391
+ ...(autoFixInfo
392
+ ? {
393
+ autoFix: {
394
+ changes: autoFixInfo.changes,
395
+ originalErrors: autoFixInfo.originalErrors,
396
+ },
397
+ }
398
+ : {}),
399
+ };
400
+ const base = textResult(message, metadata);
401
+ return { ...base, structuredContent: { type: "json", data } };
119
402
  }
120
403
  catch (error) {
121
404
  if (error instanceof ToolError) {
@@ -150,16 +433,47 @@ export const programRunnersModule = defineToolModule({
150
433
  try {
151
434
  const parsed = uploadAsmArgsSchema.parse(args);
152
435
  ctx.logger.info("Uploading assembly program", { sourceLength: parsed.program.length });
436
+ // Assemble locally to expose structured metadata
437
+ const prg = assemblyToPrg(parsed.program);
438
+ const entryAddress = prg.readUInt16LE(0);
153
439
  const result = await ctx.client.uploadAndRunAsm(parsed.program);
154
440
  if (!result.success) {
155
- throw new ToolExecutionError("C64 firmware reported failure while running assembly program", {
441
+ return toolErrorResult(new ToolExecutionError("C64 firmware reported failure while running assembly program", {
156
442
  details: extractFailureDetails(result.details),
157
- });
443
+ }));
444
+ }
445
+ // Poll for ASM execution outcome
446
+ let pollResult;
447
+ try {
448
+ const outcome = await pollForProgramOutcome("ASM", ctx.client, ctx.logger);
449
+ if (outcome.status === "crashed") {
450
+ pollResult = {
451
+ status: "crashed",
452
+ reason: outcome.reason,
453
+ };
454
+ ctx.logger.warn("Polling detected ASM program crash", pollResult);
455
+ return toolErrorResult(new ToolExecutionError("Assembly program appears to have crashed (no screen changes detected)", {
456
+ details: { reason: outcome.reason },
457
+ }));
458
+ }
459
+ }
460
+ catch (pollError) {
461
+ ctx.logger.debug("Polling encountered an error", toRecord(pollError));
158
462
  }
159
- return textResult("Assembly program assembled, uploaded, and executed successfully.", {
463
+ const data = {
464
+ kind: "upload_and_run_asm",
465
+ format: "prg",
466
+ entryAddress,
467
+ prgSize: prg.length,
468
+ resources: ["c64://specs/assembly", "c64://context/bootstrap"],
469
+ };
470
+ const base = textResult("Assembly program assembled, uploaded, and executed successfully.", {
160
471
  success: true,
472
+ entryAddress,
473
+ prgSize: prg.length,
161
474
  details: result.details ?? null,
162
475
  });
476
+ return { ...base, structuredContent: { type: "json", data } };
163
477
  }
164
478
  catch (error) {
165
479
  if (error instanceof ToolError) {
@@ -210,11 +524,20 @@ export const programRunnersModule = defineToolModule({
210
524
  details: extractFailureDetails(result.details),
211
525
  });
212
526
  }
213
- return textResult(`PRG ${parsed.path} loaded into memory.`, {
527
+ const data = {
528
+ kind: "load_prg_file",
529
+ format: "prg",
530
+ path: parsed.path,
531
+ entryAddress: null,
532
+ resources: ["c64://context/bootstrap"],
533
+ };
534
+ const base = textResult(`PRG ${parsed.path} loaded into memory.`, {
214
535
  success: true,
215
536
  path: parsed.path,
537
+ entryAddress: null,
216
538
  details: toRecord(result.details) ?? null,
217
539
  });
540
+ return { ...base, structuredContent: { type: "json", data } };
218
541
  }
219
542
  catch (error) {
220
543
  if (error instanceof ToolError) {
@@ -254,11 +577,20 @@ export const programRunnersModule = defineToolModule({
254
577
  details: extractFailureDetails(result.details),
255
578
  });
256
579
  }
257
- return textResult(`PRG ${parsed.path} loaded and executed.`, {
580
+ const data = {
581
+ kind: "run_prg_file",
582
+ format: "prg",
583
+ path: parsed.path,
584
+ entryAddress: null,
585
+ resources: ["c64://context/bootstrap"],
586
+ };
587
+ const base = textResult(`PRG ${parsed.path} loaded and executed.`, {
258
588
  success: true,
259
589
  path: parsed.path,
590
+ entryAddress: null,
260
591
  details: toRecord(result.details) ?? null,
261
592
  });
593
+ return { ...base, structuredContent: { type: "json", data } };
262
594
  }
263
595
  catch (error) {
264
596
  if (error instanceof ToolError) {
@@ -297,11 +629,19 @@ export const programRunnersModule = defineToolModule({
297
629
  details: extractFailureDetails(result.details),
298
630
  });
299
631
  }
300
- return textResult(`CRT ${parsed.path} mounted and started.`, {
632
+ const data = {
633
+ kind: "run_crt_file",
634
+ format: "crt",
635
+ path: parsed.path,
636
+ entryAddress: null,
637
+ resources: ["c64://context/bootstrap"],
638
+ };
639
+ const base = textResult(`CRT ${parsed.path} mounted and started.`, {
301
640
  success: true,
302
641
  path: parsed.path,
303
642
  details: toRecord(result.details) ?? null,
304
643
  });
644
+ return { ...base, structuredContent: { type: "json", data } };
305
645
  }
306
646
  catch (error) {
307
647
  if (error instanceof ToolError) {
package/dist/tools/rag.js CHANGED
@@ -165,14 +165,8 @@ function serializeReference(ref) {
165
165
  payload.sourceUrl = ref.sourceUrl;
166
166
  if (ref.sourceRepoUrl)
167
167
  payload.sourceRepoUrl = ref.sourceRepoUrl;
168
- if (ref.license)
169
- payload.license = ref.license;
170
168
  if (ref.licenseSpdxId)
171
- payload.licenseSpdxId = ref.licenseSpdxId;
172
- if (ref.licenseName)
173
- payload.licenseName = ref.licenseName;
174
- if (ref.licenseUrl)
175
- payload.licenseUrl = ref.licenseUrl;
169
+ payload.spdxId = ref.licenseSpdxId;
176
170
  if (ref.attribution)
177
171
  payload.attribution = ref.attribution;
178
172
  return payload;
package/doc/MCP_SETUP.md CHANGED
@@ -129,7 +129,13 @@ To exercise a real device, ensure your config points at the desired host/port an
129
129
  npm test -- --real
130
130
  ```
131
131
 
132
- `npm run coverage` captures c8 coverage for CI parity.
132
+ To run tests with coverage analysis:
133
+
134
+ ```bash
135
+ npm run coverage
136
+ ```
137
+
138
+ Note: The coverage command uses Bun internally for faster execution, but the published package remains fully compatible with Node.js.
133
139
 
134
140
  ## 6. Common Tasks
135
141
 
@@ -0,0 +1,16 @@
1
+ # Bun Migration Baseline (Node.js)
2
+
3
+ Captured before introducing Bun tooling. All commands executed on the `bun-migration` branch using Node.js 18 (system default).
4
+
5
+ | Command | Notes | Duration |
6
+ |---------|-------|----------|
7
+ | `npm run build` | TypeScript compile + postbuild + README update | **10.44 s** |
8
+ | `npm test` | Full TAP suite via `scripts/run-tests.mjs` | **47.61 s** |
9
+
10
+ Additional context:
11
+
12
+ - Dependencies installed with `npm install` (fresh, no cache) before measuring.
13
+ - No Bun tooling in use; these timings represent our current Node-only baseline.
14
+ - Results recorded on 2025-10-29 to anchor future comparisons.
15
+
16
+ Next step: use these numbers to evaluate improvements once Bun drives install/build/test tasks.
@@ -0,0 +1,55 @@
1
+ # Bun Migration Checklist
2
+
3
+ > Track the incremental migration from Node.js/npm tooling to Bun.
4
+ > Update each item as it is completed and keep a short note/date beside the checkbox.
5
+ > Before doing any work, ensure you understand the [Bun Migration Strategy](./migration.md).
6
+ > Keep notes of any deviations or important observations in the checklist itself.
7
+
8
+ ## 1. Foundations
9
+
10
+ - [x] **1.1** Capture baseline metrics (install/build/test timings, coverage reports) for comparison. *(doc/bun/baseline-metrics.md, 2025-10-29)*
11
+ - [x] **1.2** Introduce Bun toolchain alongside existing Node workflow (lockfile, `packageManager`, basic `bun install` smoke test).
12
+
13
+ ## 2. Package & Tooling
14
+
15
+ - [x] **2.1** Update `package.json` scripts to use Bun equivalents while keeping Node fallbacks where required. *(added Bun packageManager + `check:node-compat`)*
16
+ - [x] **2.2** Ensure `bun run build` reproduces the existing `dist/` layout (compare key artifacts). *(verified 2025-10-29)*
17
+ - [x] **2.3** Add an automated check that validates the published package still runs under `npm install` + `node`. *(npm script + compatibility harness)*
18
+
19
+ ## 3. Test Suite
20
+
21
+ - [x] **3.1** Migrate test runner to `bun test`, converting assertion helpers and harness utilities. *(bun harness + timeout guardrails, 2025-10-29)*
22
+ - [x] **3.2** Ensure `bun test` runs the full suite with the same semantics (mock servers, fixtures, CLI tools). *(`bun scripts/run-tests.ts --mock`, 2025-10-29)*
23
+ - [x] **3.3** Provide an explicit npm/node compatibility test (e.g., `npm test` wrapper or integration smoke test). *(`npm run test:node`, 2025-10-29)*
24
+
25
+ ## 4. Coverage & Reporting
26
+
27
+ - [x] **4.1** Replace `c8` with Bun’s coverage tooling and export lcov output. *(bun coverage emits `coverage/lcov.info`, 2025-10-29)*
28
+ - [x] **4.2** Verify Codecov integration with the new coverage artifacts. *(workflow uploads Bun lcov, 2025-10-29)*
29
+
30
+ ## 5. Scripts & Utilities
31
+
32
+ - [x] **5.1** Port repository scripts (`scripts/*.mjs`) from Node-specific loaders/`ts-node` to Bun-native execution. *(start, rag, CLI now Bun-aware; Node fallbacks use dist, 2025-10-29)*
33
+ - [x] **5.2** Replace `register-ts-node` usage with Bun-compatible module loading. *(ts-node loader inlined; helper removed, 2025-10-29)*
34
+ - [x] **5.3** Update shell helpers (e.g., `run-mcp-check.sh`, `c64-tool.sh`) to call Bun commands by default. *(preferred Bun paths with Node fallback, 2025-10-29)*
35
+
36
+ ## 6. Automation & CI
37
+
38
+ - [x] **6.1** Update GitHub Actions to install Bun (`oven-sh/setup-bun`) and run build/test/coverage using Bun. *(added setup-bun, switched to `bun run build`, ensured LCOV output for Codecov, 2025-10-30)*
39
+ - [x] **6.2** Ensure Docker-based checks (if any) consume the new Bun workflow without regressions. *(builder image now includes Bun; CI jobs use container and run Bun successfully, 2025-10-30)*
40
+
41
+ ## 7. Docker & Runtime
42
+
43
+ - [x] **7.1** Rebuild the Dockerfile using an official Bun base image; retain runtime parity and entrypoints. *(deviation: kept `ubuntu:24.04` base but installed Bun system-wide; image provides both Node and Bun for CI parity, 2025-10-30)*
44
+ - [x] **7.2** Confirm resulting image passes existing smoke tests and compatibility checks. *(Docker workflow builds `mcp-builder` and downstream package smoke tests pass, 2025-10-30)*
45
+
46
+ ## 8. Documentation
47
+
48
+ - [x] **8.1** Refresh README, developer docs, and setup guides to reference Bun (with a note about Node compatibility). *(README Build & Test updated to include Bun; dev docs already referenced Bun, 2025-10-30)*
49
+ - [x] **8.2** Document the npm/node compatibility verification process for external consumers. *(`npm run check:node-compat` documented in dev docs; retained, 2025-10-30)*
50
+
51
+ ## 9. Final Verification
52
+
53
+ - [x] **9.1** Run full validation suite: `bun install`, `bun run build`, `bun test --coverage`, npm compatibility smoke test. *(wired in CI; local scripts use Bun runner; coverage emits LCOV, 2025-10-30)*
54
+ - [x] **9.2** Compare final artifacts and confirm no API/package layout changes. *(build parity verified earlier; CI keeps packaging check, 2025-10-30)*
55
+ - [x] **9.3** Remove obsolete Node-only tooling and close out migration notes. *(no remaining Node-only paths required for CI; kept Node paths for consumer compatibility, 2025-10-30)*
@@ -0,0 +1,83 @@
1
+ # Bun Migration Strategy
2
+
3
+ Our goal is to adopt Bun everywhere it improves speed (local dev, CI, container builds) **without sacrificing Node compatibility** for downstream consumers.
4
+
5
+ ---
6
+
7
+ ## Approach Overview
8
+
9
+ 1. **Track incremental work via the checklist** (`doc/bun/migration-checklist.md`).
10
+ 2. **Introduce Bun tooling in parallel** with Node/npm. Validate parity before turning off Node paths.
11
+ 3. **Port the build/test pipeline** to Bun (`bun install`, `bun run build`, `bun test --coverage`) and keep comparing against Node outputs (especially `dist/` packaging).
12
+ 4. **Update automation and docs** once Bun passes all checks.
13
+ 5. **Retain Node smoke tests** to ensure published artifacts remain consumable by Node users.
14
+
15
+ ### Current Status (2025-10-29)
16
+
17
+ - Baseline Node metrics captured (`npm run build` ~10.44 s, `npm test` ~47.61 s) in `doc/bun/baseline-metrics.md`.
18
+ - Bun toolchain bootstrapped alongside Node (`bun.lock`, `packageManager`, smoke `bun install`).
19
+ - Added `npm run check:node-compat` (packs the module, installs via npm, and imports key ESM modules with Node) to guard compatibility as we iterate.
20
+ - `bun run build` verified to produce the same `dist/` output as the Node workflow.
21
+
22
+ ---
23
+
24
+ ## Guardrails
25
+
26
+ ### 1. Build & Runtime Rules
27
+
28
+ - Sources remain TypeScript, emitting standard ESM/CJS modules compatible with Node.
29
+ - Published code may not depend on Bun-only APIs (no `Bun.file`, `Bun.serve`, etc.).
30
+ - The compiled `dist/` must execute under both **Bun ≥1.1** and **Node ≥18**.
31
+ - `package.json` stays valid for npm clients (no Bun-only metadata).
32
+
33
+ ### 2. CI Strategy (Asymmetric Validation)
34
+
35
+ - **Primary path (PRs, default CI):**
36
+
37
+ ```bash
38
+ bun install
39
+ bun run build
40
+ bun test --coverage
41
+ ```
42
+
43
+ - **Release/nightly Node validation (reuse `dist/`):**
44
+
45
+ ```bash
46
+ npm ci --omit=dev
47
+ node dist/index.js --version
48
+ npm test -- --smoke
49
+ ```
50
+
51
+ ### 3. Docker Strategy
52
+
53
+ Build with Bun, run with Node:
54
+
55
+ ```dockerfile
56
+ FROM oven/bun:latest AS build
57
+ WORKDIR /app
58
+ COPY . .
59
+ RUN bun install && bun run build
60
+
61
+ FROM node:20-slim
62
+ WORKDIR /app
63
+ COPY --from=build /app/dist ./dist
64
+ COPY package.json .
65
+ CMD ["node", "dist/index.js"]
66
+ ```
67
+
68
+ ### 4. Validation Matrix
69
+
70
+ - **Bun:** install/build/test/coverage on every PR.
71
+ - **Node:** smoke tests on main branch, releases, or nightly jobs.
72
+ - **Rule:** only build once; reuse Bun artifacts for Node checks.
73
+
74
+ ### 5. Documentation Messaging
75
+ >
76
+ > Built and tested with **Bun** ⚡
77
+ > Fully compatible with **Node.js 18+** 🟩
78
+
79
+ ---
80
+
81
+ ## Next Steps
82
+
83
+ Use this document together with the checklist to plan each migration phase. Update both as milestones are completed and ensure the guardrails stay enforced.