assertledger 1.1.1 → 1.3.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 (77) hide show
  1. package/README.fr.md +34 -4
  2. package/README.md +33 -4
  3. package/conformance/schema-extensions.json +25 -0
  4. package/dist/build-info.json +1 -1
  5. package/dist/cli.d.ts +8 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +292 -25
  8. package/dist/cli.js.map +1 -1
  9. package/dist/contracts/index.d.ts +510 -4
  10. package/dist/contracts/index.d.ts.map +1 -1
  11. package/dist/contracts/index.js +230 -16
  12. package/dist/contracts/index.js.map +1 -1
  13. package/dist/contracts/runtime-doctor.d.ts +123 -0
  14. package/dist/contracts/runtime-doctor.d.ts.map +1 -1
  15. package/dist/contracts/runtime-doctor.js +64 -0
  16. package/dist/contracts/runtime-doctor.js.map +1 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +5 -2
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/diagnostics.d.ts.map +1 -1
  21. package/dist/diagnostics.js +26 -2
  22. package/dist/diagnostics.js.map +1 -1
  23. package/dist/engine/adapters/bun-test-profile.d.ts +17 -0
  24. package/dist/engine/adapters/bun-test-profile.d.ts.map +1 -0
  25. package/dist/engine/adapters/bun-test-profile.js +17 -0
  26. package/dist/engine/adapters/bun-test-profile.js.map +1 -0
  27. package/dist/engine/connection.d.ts +25 -1
  28. package/dist/engine/connection.d.ts.map +1 -1
  29. package/dist/engine/connection.js +144 -31
  30. package/dist/engine/connection.js.map +1 -1
  31. package/dist/engine/demo.d.ts +16 -0
  32. package/dist/engine/demo.d.ts.map +1 -0
  33. package/dist/engine/demo.js +51 -0
  34. package/dist/engine/demo.js.map +1 -0
  35. package/dist/engine/git-regression.d.ts +2 -2
  36. package/dist/engine/git-regression.d.ts.map +1 -1
  37. package/dist/engine/git-regression.js.map +1 -1
  38. package/dist/engine/index.d.ts +26 -5
  39. package/dist/engine/index.d.ts.map +1 -1
  40. package/dist/engine/index.js +539 -58
  41. package/dist/engine/index.js.map +1 -1
  42. package/dist/engine/runtime-doctor.d.ts +13 -4
  43. package/dist/engine/runtime-doctor.d.ts.map +1 -1
  44. package/dist/engine/runtime-doctor.js +80 -2
  45. package/dist/engine/runtime-doctor.js.map +1 -1
  46. package/dist/engine/setup.d.ts +39 -0
  47. package/dist/engine/setup.d.ts.map +1 -0
  48. package/dist/engine/setup.js +411 -0
  49. package/dist/engine/setup.js.map +1 -0
  50. package/dist/mcp/index.d.ts.map +1 -1
  51. package/dist/mcp/index.js +36 -6
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/sdk/index.d.ts +7 -6
  54. package/dist/sdk/index.d.ts.map +1 -1
  55. package/dist/sdk/index.js +20 -7
  56. package/dist/sdk/index.js.map +1 -1
  57. package/docs/adapter-protocol.md +61 -0
  58. package/docs/architecture.md +4 -1
  59. package/docs/developer-experience.md +39 -5
  60. package/docs/distribution.md +8 -3
  61. package/docs/migration-verification-v3.md +33 -0
  62. package/docs/project-intent.md +7 -5
  63. package/docs/reference.md +9 -5
  64. package/docs/repository-init.md +52 -7
  65. package/docs/runtime-doctor.md +12 -9
  66. package/integrations/bun/assertions.d.mts +4 -0
  67. package/integrations/bun/assertions.mjs +25 -0
  68. package/integrations/bun/driver.d.mts +27 -0
  69. package/integrations/bun/driver.mjs +383 -0
  70. package/integrations/bun/preload.mjs +165 -0
  71. package/integrations/skill/SKILL.md +4 -1
  72. package/package.json +7 -3
  73. package/schemas/evidence-manifest.v3.json +897 -0
  74. package/schemas/repository-init-config.v2.json +210 -0
  75. package/schemas/repository-init-lock.v2.json +162 -0
  76. package/schemas/repository-init-result.v2.json +212 -0
  77. package/schemas/verification-request.v3.json +484 -0
@@ -1,25 +1,29 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { createReadStream } from "node:fs";
4
- import { cp, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
4
+ import { cp, link, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
- import { pathToFileURL } from "node:url";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { createScanner, SyntaxKind } from "typescript/unstable/ast";
11
- import { AdapterSchema, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitLock, parseRepositoryInitResult, parseVerificationRequest, parseVersionedVerificationRequest, repositoryInitConfigDigest, repositoryInitLockDigest, } from "../contracts/index.js";
11
+ import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVerificationRequest, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, } from "../contracts/index.js";
12
12
  import { assertSafeRelativePath, canonicalize, createAgenticBenchmark, decideEvidence, portablePathKey, replayAgenticBenchmark, replayEvidenceManifest, sealManifestArtifact, sha256Canonical, } from "../core/index.js";
13
+ import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
13
14
  import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
14
15
  import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
15
- import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
16
16
  import { normalizeRuntimeFacts, RUNTIME_FACTS_VERSION } from "./adapters/runtime-facts.js";
17
+ import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
17
18
  import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
18
- import { runRuntimeDoctorChecks } from "./runtime-doctor.js";
19
+ import { runBunRuntimeDoctorChecks, runRuntimeDoctorChecks, } from "./runtime-doctor.js";
19
20
  const DEFAULT_EXCLUDES = new Set([".git", ".testforge", "node_modules"]);
20
21
  const SHA256_PREFIX = "sha256:";
21
22
  const PROCESS_TERMINATION_GRACE_MS = 250;
22
23
  const CONTROLLED_REPORT_MAXIMUM_BYTES = 64 * 1024;
24
+ const BUN_TEST_DRIVER_PATH = fileURLToPath(new URL("../../integrations/bun/driver.mjs", import.meta.url));
25
+ const BUN_TEST_HELPER_PATH = fileURLToPath(new URL("../../integrations/bun/assertions.mjs", import.meta.url));
26
+ const BUN_TEST_PRELOAD_PATH = fileURLToPath(new URL("../../integrations/bun/preload.mjs", import.meta.url));
23
27
  const TEMPORARY_CLEANUP_OPTIONS = {
24
28
  recursive: true,
25
29
  force: true,
@@ -31,6 +35,20 @@ const INIT_LOCK_FILE = "assertledger.lock.json";
31
35
  const INIT_MANAGED_FILES = new Set([INIT_CONFIG_FILE, INIT_LOCK_FILE]);
32
36
  const INIT_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun", "uv", "poetry", "pip"];
33
37
  const INIT_FRAMEWORKS = ["node:test", "vitest", "jest", "bun:test", "pytest"];
38
+ export class RepositoryInitWriteError extends Error {
39
+ targetPath;
40
+ temporaryPath;
41
+ temporaryCleanup;
42
+ installedPaths;
43
+ constructor(cause, targetPath, temporaryPath, temporaryCleanup, installedPaths) {
44
+ super("INIT_WRITE_FAILED", { cause });
45
+ this.name = "RepositoryInitWriteError";
46
+ this.targetPath = targetPath;
47
+ this.temporaryPath = temporaryPath;
48
+ this.temporaryCleanup = temporaryCleanup;
49
+ this.installedPaths = installedPaths;
50
+ }
51
+ }
34
52
  async function removeTemporaryDirectory(directory) {
35
53
  await rm(directory, TEMPORARY_CLEANUP_OPTIONS);
36
54
  }
@@ -280,6 +298,32 @@ function registerPortablePath(spellings, relative) {
280
298
  throw new Error("PORTABLE_PATH_COLLISION");
281
299
  spellings.set(key, relative);
282
300
  }
301
+ /** The declared list of a valid configuration; anything unreadable falls back to the defaults,
302
+ * which only widens the inventory and so keeps every link check. */
303
+ async function configuredRepositoryExcludes(root) {
304
+ try {
305
+ const configPath = path.join(root, INIT_CONFIG_FILE);
306
+ if (!(await lstat(configPath)).isFile())
307
+ return [];
308
+ const value = JSON.parse(await readFile(configPath, "utf8"));
309
+ return parseVersionedRepositoryInitConfig(value).repository.exclude;
310
+ }
311
+ catch {
312
+ return [];
313
+ }
314
+ }
315
+ async function configuredRepositoryFramework(root) {
316
+ try {
317
+ const configPath = path.join(root, INIT_CONFIG_FILE);
318
+ if (!(await lstat(configPath)).isFile())
319
+ return undefined;
320
+ const value = JSON.parse(await readFile(configPath, "utf8"));
321
+ return parseVersionedRepositoryInitConfig(value).framework;
322
+ }
323
+ catch {
324
+ return undefined;
325
+ }
326
+ }
283
327
  async function walkFiles(root, excludes) {
284
328
  const files = [];
285
329
  const spellings = new Map();
@@ -419,6 +463,9 @@ function initEvidenceKind(file) {
419
463
  function initNodeTestSource(file) {
420
464
  return /(?:^|\/)[^/]+\.test\.[cm]?[jt]s$/u.test(file);
421
465
  }
466
+ function initBunTestSource(file) {
467
+ return /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(file);
468
+ }
422
469
  function initSafePathPattern(value) {
423
470
  if (value.startsWith("-") || value.includes("\\"))
424
471
  return false;
@@ -462,7 +509,10 @@ function initPathPatternMatcher(pattern) {
462
509
  }
463
510
  function initCommandPathMatchers(command, testSources) {
464
511
  const conventionalDirectories = new Set(["test", "tests", "spec", "specs", "__tests__"]);
465
- return command.arguments
512
+ const operands = command.executable === "bun" && command.arguments[0] === "test"
513
+ ? command.arguments.slice(1)
514
+ : command.arguments;
515
+ return operands
466
516
  .filter((argument) => initSafePathPattern(argument) &&
467
517
  (/[*?]/u.test(argument) ||
468
518
  argument.includes("/") ||
@@ -548,16 +598,75 @@ function initJavaScriptModuleSpecifiers(source) {
548
598
  specifiers.delete("");
549
599
  return specifiers;
550
600
  }
551
- async function atomicInitWrite(root, relative, content) {
601
+ async function atomicInitWrite(root, relative, content, action, installedPaths, dependencies = {}) {
552
602
  const target = path.join(root, relative);
553
603
  const temporary = path.join(root, `.${relative}.${process.pid}.${Date.now()}.tmp`);
604
+ const openTemporary = dependencies.openTemporary ?? ((temporaryPath) => open(temporaryPath, "wx"));
605
+ const writeTemporary = dependencies.writeTemporary ??
606
+ ((_temporaryPath, temporaryContent, handle) => handle.writeFile(temporaryContent));
607
+ const renameTemporary = dependencies.renameTemporary ?? rename;
608
+ const linkTemporary = dependencies.linkTemporary ?? link;
609
+ const removeTemporary = dependencies.removeTemporary ?? ((temporaryPath) => rm(temporaryPath, { force: true }));
610
+ let temporaryOwned = false;
611
+ let temporaryHandle;
612
+ let temporaryHandleClosed = true;
613
+ let targetInstalled = false;
554
614
  try {
555
- await writeFile(temporary, content, { flag: "wx" });
556
- await rename(temporary, target);
615
+ temporaryHandle = await openTemporary(temporary);
616
+ temporaryOwned = true;
617
+ temporaryHandleClosed = false;
618
+ try {
619
+ await writeTemporary(temporary, content, temporaryHandle);
620
+ }
621
+ finally {
622
+ await temporaryHandle.close();
623
+ temporaryHandleClosed = true;
624
+ }
625
+ if (action === "CREATE") {
626
+ await linkTemporary(temporary, target);
627
+ targetInstalled = true;
628
+ await removeTemporary(temporary);
629
+ }
630
+ else {
631
+ await renameTemporary(temporary, target);
632
+ targetInstalled = true;
633
+ }
557
634
  }
558
635
  catch (error) {
559
- await rm(temporary, { force: true });
560
- throw error;
636
+ let temporaryCleanup = "NOT_OWNED";
637
+ if (!temporaryOwned) {
638
+ try {
639
+ await lstat(temporary);
640
+ temporaryCleanup = "UNRESOLVED";
641
+ }
642
+ catch (inspectionError) {
643
+ if (inspectionError.code !== "ENOENT") {
644
+ temporaryCleanup = "UNRESOLVED";
645
+ }
646
+ }
647
+ }
648
+ if (temporaryOwned && !temporaryHandleClosed && temporaryHandle !== undefined) {
649
+ try {
650
+ await temporaryHandle.close();
651
+ temporaryHandleClosed = true;
652
+ }
653
+ catch {
654
+ temporaryCleanup = "UNRESOLVED";
655
+ }
656
+ }
657
+ if (temporaryOwned) {
658
+ temporaryCleanup = "REMOVED";
659
+ try {
660
+ await removeTemporary(temporary);
661
+ }
662
+ catch {
663
+ temporaryCleanup = "UNRESOLVED";
664
+ }
665
+ }
666
+ throw new RepositoryInitWriteError(error, target, temporary, temporaryCleanup, [
667
+ ...installedPaths,
668
+ ...(targetInstalled ? [target] : []),
669
+ ]);
561
670
  }
562
671
  }
563
672
  function initEmptyDetections(reasonCodes) {
@@ -571,8 +680,9 @@ function initEmptyDetections(reasonCodes) {
571
680
  };
572
681
  }
573
682
  function initTerminalResult(status, detections, reasonCodes) {
574
- return parseRepositoryInitResult({
575
- schemaVersion: "1.0.0",
683
+ const bunBuiltIn = detections.adapterRecommendation === "bun-test";
684
+ const result = {
685
+ schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
576
686
  status,
577
687
  reasonCodes: [...new Set(reasonCodes)].sort(),
578
688
  detections: { ...detections, reasonCodes: [...new Set(detections.reasonCodes)].sort() },
@@ -580,9 +690,10 @@ function initTerminalResult(status, detections, reasonCodes) {
580
690
  files: [],
581
691
  requiredOperatorInputs: ["worlds", "candidates"],
582
692
  nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
583
- });
693
+ };
694
+ return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
584
695
  }
585
- export async function initializeRepository(requestedRoot, options = {}) {
696
+ export async function initializeRepository(requestedRoot, options = {}, writeDependencies = {}) {
586
697
  let root;
587
698
  try {
588
699
  root = await realpath(requestedRoot);
@@ -594,13 +705,32 @@ export async function initializeRepository(requestedRoot, options = {}) {
594
705
  "REPOSITORY_ROOT_INVALID",
595
706
  ]);
596
707
  }
708
+ const declaredExcludes = options.exclude ?? (await configuredRepositoryExcludes(root));
709
+ const repositoryExclude = [...new Set([...DEFAULT_EXCLUDES, ...declaredExcludes])].sort();
710
+ if (repositoryExclude.length > 1_000 ||
711
+ options.exclude?.some((entry) => !isRepositoryExcludeName(entry))) {
712
+ return initTerminalResult("CONFLICT", initEmptyDetections(["INVALID_REPOSITORY_EXCLUDE"]), [
713
+ "INVALID_REPOSITORY_EXCLUDE",
714
+ ]);
715
+ }
716
+ const inventoryExcludes = effectiveExcludes([...INIT_MANAGED_FILES, ...repositoryExclude]);
597
717
  const candidateRoots = ["tests/candidates"];
598
718
  const outsideCandidateRoots = (file) => !candidateRoots.some((candidateRoot) => {
599
719
  const rootKey = portablePathKey(candidateRoot);
600
720
  const fileKey = portablePathKey(file);
601
721
  return fileKey === rootKey || fileKey.startsWith(`${rootKey}/`);
602
722
  });
603
- const inventory = await walkFiles(root, effectiveExcludes([...INIT_MANAGED_FILES]));
723
+ let inventory;
724
+ try {
725
+ inventory = await walkFiles(root, inventoryExcludes);
726
+ }
727
+ catch (error) {
728
+ if (!(error instanceof Error) || error.message !== "UNSUPPORTED_REPOSITORY_SYMLINK")
729
+ throw error;
730
+ return initTerminalResult("CONFLICT", initEmptyDetections(["UNSUPPORTED_REPOSITORY_SYMLINK"]), [
731
+ "UNSUPPORTED_REPOSITORY_SYMLINK",
732
+ ]);
733
+ }
604
734
  const inScopeFiles = inventory.files.filter(outsideCandidateRoots);
605
735
  const contents = new Map();
606
736
  const evidenceBytes = new Map();
@@ -713,7 +843,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
713
843
  (contents.has("requirements.txt") &&
714
844
  /(^|\n)pytest(?:[=<>!~]|$)/u.test(contents.get("requirements.txt") ?? "")))
715
845
  frameworkFacts.add("pytest");
716
- const frameworkOverride = options.framework;
846
+ const frameworkOverride = options.framework ?? (await configuredRepositoryFramework(root));
717
847
  if (frameworkOverride !== undefined && !isInitFramework(frameworkOverride)) {
718
848
  const detections = initEmptyDetections(["FRAMEWORK_OVERRIDE_INVALID"]);
719
849
  detections.packageManager = packageManager;
@@ -825,8 +955,24 @@ export async function initializeRepository(requestedRoot, options = {}) {
825
955
  adapterRecommendation = "node-test";
826
956
  }
827
957
  }
958
+ else if (framework === "bun:test") {
959
+ const testSources = inScopeFiles.filter((file) => initBunTestSource(file) &&
960
+ initJavaScriptModuleSpecifiers(contents.get(file) ?? "").has("bun:test"));
961
+ const pathMatchers = initCommandPathMatchers(testCommand, testSources);
962
+ const baseTestFiles = pathMatchers.length === 0
963
+ ? testSources
964
+ : testSources.filter((file) => pathMatchers.some((matcher) => matcher(file)));
965
+ if (baseTestFiles.length > 0) {
966
+ adapter = { kind: "bun-test", executable: "bun", baseTestFiles };
967
+ adapterRecommendation = "bun-test";
968
+ }
969
+ }
828
970
  const reasonCodes = adapter === undefined
829
- ? [framework === "node:test" ? "BASE_TEST_FILES_UNAVAILABLE" : "OFFICIAL_ADAPTER_UNAVAILABLE"]
971
+ ? [
972
+ framework === "node:test" || framework === "bun:test"
973
+ ? "BASE_TEST_FILES_UNAVAILABLE"
974
+ : "OFFICIAL_ADAPTER_UNAVAILABLE",
975
+ ]
830
976
  : [];
831
977
  const detections = {
832
978
  packageManager,
@@ -839,17 +985,23 @@ export async function initializeRepository(requestedRoot, options = {}) {
839
985
  if (adapter === undefined)
840
986
  return initTerminalResult("BLOCKED", detections, reasonCodes);
841
987
  await options.afterEvidenceSnapshot?.();
842
- const config = parseRepositoryInitConfig({
843
- schemaVersion: "1.0.0",
844
- repository: { root: ".", exclude: [".git", ".testforge", "node_modules"] },
988
+ const bunBuiltIn = adapter.kind === "bun-test";
989
+ const configValue = {
990
+ schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
991
+ repository: { root: ".", exclude: repositoryExclude },
845
992
  packageManager,
846
993
  framework,
847
994
  testCommand,
848
995
  adapter,
849
996
  candidateRoots: [...candidateRoots],
850
- });
997
+ };
998
+ const config = bunBuiltIn
999
+ ? parseRepositoryInitConfigV2(configValue)
1000
+ : parseRepositoryInitConfig(configValue);
851
1001
  const configContent = initJsonBytes(config);
852
- const configDigest = repositoryInitConfigDigest(config);
1002
+ const configDigest = config.schemaVersion === "2.0.0"
1003
+ ? repositoryInitConfigV2Digest(config)
1004
+ : repositoryInitConfigDigest(config);
853
1005
  const evidenceFiles = inScopeFiles.filter((file) => initEvidenceKind(file) !== undefined);
854
1006
  if (adapterEvidencePath !== undefined && !evidenceFiles.includes(adapterEvidencePath))
855
1007
  evidenceFiles.push(adapterEvidencePath);
@@ -867,17 +1019,33 @@ export async function initializeRepository(requestedRoot, options = {}) {
867
1019
  : (initEvidenceKind(file) ?? "ADAPTER_CONFIG"),
868
1020
  });
869
1021
  }
870
- const lockBase = {
871
- schemaVersion: "1.0.0",
872
- configDigest,
873
- detector: { name: "assertledger-init", version: "1.0.0" },
874
- evidence,
875
- detections,
876
- };
877
- const lock = parseRepositoryInitLock({
878
- ...lockBase,
879
- lockDigest: repositoryInitLockDigest(lockBase),
880
- });
1022
+ let lock;
1023
+ if (bunBuiltIn) {
1024
+ const lockBase = {
1025
+ schemaVersion: "2.0.0",
1026
+ configDigest,
1027
+ detector: { name: "assertledger-init", version: "2.0.0" },
1028
+ evidence,
1029
+ detections,
1030
+ };
1031
+ lock = parseRepositoryInitLockV2({
1032
+ ...lockBase,
1033
+ lockDigest: repositoryInitLockV2Digest(lockBase),
1034
+ });
1035
+ }
1036
+ else {
1037
+ const lockBase = {
1038
+ schemaVersion: "1.0.0",
1039
+ configDigest,
1040
+ detector: { name: "assertledger-init", version: "1.0.0" },
1041
+ evidence,
1042
+ detections: RepositoryInitDetectionsSchema.parse(detections),
1043
+ };
1044
+ lock = parseRepositoryInitLock({
1045
+ ...lockBase,
1046
+ lockDigest: repositoryInitLockDigest(lockBase),
1047
+ });
1048
+ }
881
1049
  const lockContent = initJsonBytes(lock);
882
1050
  const plannedFiles = [
883
1051
  { path: INIT_CONFIG_FILE, digest: rawSha256(configContent), content: configContent },
@@ -885,7 +1053,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
885
1053
  ];
886
1054
  let finalInScopeFiles;
887
1055
  try {
888
- const finalInventory = await walkFiles(root, effectiveExcludes([...INIT_MANAGED_FILES]));
1056
+ const finalInventory = await walkFiles(root, inventoryExcludes);
889
1057
  finalInScopeFiles = finalInventory.files.filter(outsideCandidateRoots);
890
1058
  }
891
1059
  catch {
@@ -940,7 +1108,7 @@ export async function initializeRepository(requestedRoot, options = {}) {
940
1108
  let structural;
941
1109
  try {
942
1110
  const value = JSON.parse(existingLock);
943
- structural = parseRepositoryInitLock(value);
1111
+ structural = parseVersionedRepositoryInitLock(value);
944
1112
  }
945
1113
  catch {
946
1114
  return initTerminalResult("CONFLICT", { ...detections, reasonCodes: ["LOCK_INVALID"] }, [
@@ -952,8 +1120,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
952
1120
  }
953
1121
  }
954
1122
  if (existingConfig === configContent && existingLock === lockContent) {
955
- return parseRepositoryInitResult({
956
- schemaVersion: "1.0.0",
1123
+ const result = {
1124
+ schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
957
1125
  status: "UNCHANGED",
958
1126
  reasonCodes: [],
959
1127
  detections,
@@ -961,7 +1129,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
961
1129
  files: plannedFiles,
962
1130
  requiredOperatorInputs: ["worlds", "candidates"],
963
1131
  nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
964
- });
1132
+ };
1133
+ return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
965
1134
  }
966
1135
  const actions = [];
967
1136
  if (existingConfig === undefined)
@@ -972,8 +1141,8 @@ export async function initializeRepository(requestedRoot, options = {}) {
972
1141
  actions.push({ kind: "REGENERATE", path: INIT_LOCK_FILE });
973
1142
  actions.sort((left, right) => left.path.localeCompare(right.path));
974
1143
  const status = options.dryRun ? "WOULD_CREATE" : "CREATED";
975
- const result = parseRepositoryInitResult({
976
- schemaVersion: "1.0.0",
1144
+ const resultValue = {
1145
+ schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
977
1146
  status,
978
1147
  reasonCodes: [],
979
1148
  detections,
@@ -981,13 +1150,19 @@ export async function initializeRepository(requestedRoot, options = {}) {
981
1150
  files: plannedFiles,
982
1151
  requiredOperatorInputs: ["worlds", "candidates"],
983
1152
  nextCommands: [{ executable: "assertledger", arguments: ["audit", ".", "--json"] }],
984
- });
1153
+ };
1154
+ const result = bunBuiltIn
1155
+ ? parseRepositoryInitResultV2(resultValue)
1156
+ : parseRepositoryInitResult(resultValue);
985
1157
  if (!options.dryRun) {
1158
+ const installedPaths = [];
986
1159
  for (const action of actions) {
987
1160
  const file = plannedFiles.find((candidate) => candidate.path === action.path);
988
1161
  if (file === undefined)
989
1162
  throw new Error("INIT_PLAN_INCONSISTENT");
990
- await atomicInitWrite(root, file.path, file.content);
1163
+ await atomicInitWrite(root, file.path, file.content, action.kind, installedPaths, writeDependencies);
1164
+ if (action.kind === "CREATE")
1165
+ installedPaths.push(path.join(root, file.path));
991
1166
  }
992
1167
  }
993
1168
  return result;
@@ -1029,9 +1204,52 @@ async function probeRuntimeTemporaryWorkspace() {
1029
1204
  }
1030
1205
  export async function doctorRepositoryRuntime(requestedRoot, options) {
1031
1206
  const repositoryRoot = path.resolve(requestedRoot);
1207
+ const configuration = options.allowUnsafeExecution
1208
+ ? await initializeRepository(repositoryRoot, { dryRun: true })
1209
+ : undefined;
1210
+ if (configuration?.schemaVersion === "2.0.0" &&
1211
+ configuration.detections.adapterRecommendation === "bun-test") {
1212
+ const configFile = configuration.files.find((file) => file.path === INIT_CONFIG_FILE);
1213
+ if (configFile === undefined)
1214
+ throw new Error("BUN_TEST_CONFIG_MISSING");
1215
+ const config = parseRepositoryInitConfigV2(JSON.parse(configFile.content));
1216
+ const environmentAllowlist = ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"];
1217
+ let resolvedExecutable = config.adapter.executable;
1218
+ return runBunRuntimeDoctorChecks(repositoryRoot, configuration, {
1219
+ probeExecutable: async () => {
1220
+ const identity = await probeBunTestExecutable(config.adapter.executable, repositoryRoot, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
1221
+ resolvedExecutable = identity.resolvedExecutable;
1222
+ return { bunVersion: identity.bunVersion };
1223
+ },
1224
+ probeDependencies: async () => {
1225
+ const result = await runProcess({
1226
+ executable: resolvedExecutable,
1227
+ args: ["-e", 'import("bun:test").then(() => process.stdout.write("READY"))'],
1228
+ cwd: repositoryRoot,
1229
+ environment: environmentFromAllowlist(environmentAllowlist),
1230
+ timeoutMs: 5_000,
1231
+ maximumOutputBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
1232
+ });
1233
+ if (result.outcome !== "PASS" ||
1234
+ result.stdout.text !== "READY" ||
1235
+ result.stderr.totalBytes !== 0) {
1236
+ throw new Error("BUN_TEST_DEPENDENCY_UNAVAILABLE");
1237
+ }
1238
+ },
1239
+ probeTemporaryWorkspace: probeRuntimeTemporaryWorkspace,
1240
+ runSyntheticPreflight: async () => {
1241
+ await runBunTestRuntimePreflight(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
1242
+ },
1243
+ });
1244
+ }
1032
1245
  let resolvedExecutable = process.execPath;
1033
1246
  return runRuntimeDoctorChecks(repositoryRoot, options.allowUnsafeExecution, {
1034
- inspectConfiguration: () => initializeRepository(repositoryRoot, { dryRun: true }),
1247
+ inspectConfiguration: async () => {
1248
+ const inspected = configuration ?? (await initializeRepository(repositoryRoot, { dryRun: true }));
1249
+ if (inspected.schemaVersion !== "1.0.0")
1250
+ throw new Error("RUNTIME_CONFIGURATION_UNAVAILABLE");
1251
+ return inspected;
1252
+ },
1035
1253
  probeExecutable: async () => {
1036
1254
  const identity = await probeNodeTestExecutable(process.execPath, repositoryRoot, [], 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
1037
1255
  resolvedExecutable = identity.resolvedExecutable;
@@ -1368,9 +1586,10 @@ async function resolveRepositoryRoot(rootInput) {
1368
1586
  throw new TypeError("INVALID_REPOSITORY_ROOT");
1369
1587
  }
1370
1588
  }
1371
- export async function analyzeRepository(rootInput) {
1589
+ export async function analyzeRepository(rootInput, options = {}) {
1372
1590
  const root = await resolveRepositoryRoot(rootInput);
1373
- const { files } = await walkFiles(root, effectiveExcludes([]));
1591
+ const declaredExcludes = options.configuredExcludes === true ? await configuredRepositoryExcludes(root) : [];
1592
+ const { files } = await walkFiles(root, effectiveExcludes(declaredExcludes));
1374
1593
  const digest = createHash("sha256");
1375
1594
  const languageCounts = new Map();
1376
1595
  const frameworks = new Set();
@@ -1777,7 +1996,9 @@ function parseRequest(value) {
1777
1996
  if (candidateRoots.length === 0)
1778
1997
  throw new TypeError("INVALID_CANDIDATE_ROOTS");
1779
1998
  assertPortableStringCollection(candidateRoots, "DUPLICATE_CANDIDATE_ROOT");
1780
- if (adapter.kind !== "node-test" && adapter.kind !== "testforge-command") {
1999
+ if (adapter.kind !== "node-test" &&
2000
+ adapter.kind !== "bun-test" &&
2001
+ adapter.kind !== "testforge-command") {
1781
2002
  throw new TypeError("UNSUPPORTED_ADAPTER");
1782
2003
  }
1783
2004
  // The versioned contract already restricts container isolation to v2 requests.
@@ -1896,13 +2117,15 @@ function parseRequest(value) {
1896
2117
  if (minimumTargetWeightPermille > 1_000) {
1897
2118
  throw new TypeError("INVALID_MINIMUM_TARGET_WEIGHT_PERMILLE");
1898
2119
  }
1899
- const normalizedAdapter = adapter.kind === "node-test"
2120
+ const normalizedAdapter = adapter.kind === "node-test" || adapter.kind === "bun-test"
1900
2121
  ? (() => {
1901
- const extraArguments = parseNodeTestExtraArguments(adapter.extraArguments);
2122
+ const extraArguments = adapter.kind === "node-test"
2123
+ ? parseNodeTestExtraArguments(adapter.extraArguments)
2124
+ : undefined;
1902
2125
  const baseTestFiles = requireStringArray(adapter.baseTestFiles, "base_test_files").map((file) => assertSafeRelativePath(file, ["."]));
1903
2126
  assertPortableStringCollection(baseTestFiles, "DUPLICATE_BASE_TEST_FILE");
1904
2127
  return {
1905
- kind: "node-test",
2128
+ kind: adapter.kind,
1906
2129
  executable: requireString(adapter.executable, "adapter_executable"),
1907
2130
  baseTestFiles,
1908
2131
  ...(extraArguments === undefined ? {} : { extraArguments }),
@@ -2099,6 +2322,19 @@ async function readStructuredCommandReport(resultFile, processResult, maximumByt
2099
2322
  }
2100
2323
  return parseStructuredCommandReport(await readBoundedJsonFile(resultFile, maximumBytes), processResult);
2101
2324
  }
2325
+ function readBunDriverReport(processResult) {
2326
+ if (processResult.outcome === "TIMEOUT" ||
2327
+ processResult.outcome === "INFRA_ERROR" ||
2328
+ processResult.stdout.truncated ||
2329
+ processResult.stdout.totalBytes > CONTROLLED_REPORT_MAXIMUM_BYTES)
2330
+ return undefined;
2331
+ try {
2332
+ return parseStructuredCommandReport(JSON.parse(processResult.stdout.text), processResult);
2333
+ }
2334
+ catch {
2335
+ return undefined;
2336
+ }
2337
+ }
2102
2338
  async function readNodeTestReport(resultFile, processResult, maximumBytes) {
2103
2339
  if (processResult.outcome === "TIMEOUT" || processResult.outcome === "INFRA_ERROR") {
2104
2340
  return undefined;
@@ -2179,6 +2415,71 @@ async function probeNodeTestExecutable(requestedExecutable, repositoryRoot, envi
2179
2415
  nodeVersion: secondProbe.nodeVersion,
2180
2416
  };
2181
2417
  }
2418
+ async function runBunExecutableProbe(executable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
2419
+ const result = await runProcess({
2420
+ executable,
2421
+ args: [
2422
+ "-e",
2423
+ "console.log(JSON.stringify({execPath:process.execPath,bunVersion:Bun.version,bunRevision:Bun.revision}))",
2424
+ ],
2425
+ cwd: repositoryRoot,
2426
+ environment: environmentFromAllowlist(environmentAllowlist),
2427
+ timeoutMs: Math.min(timeoutMs, 5_000),
2428
+ maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
2429
+ });
2430
+ if (result.outcome !== "PASS" || result.stdout.truncated || result.stderr.totalBytes !== 0) {
2431
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2432
+ }
2433
+ let report;
2434
+ try {
2435
+ report = JSON.parse(result.stdout.text.trim());
2436
+ }
2437
+ catch {
2438
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2439
+ }
2440
+ if (!isRecord(report) ||
2441
+ Object.keys(report).sort().join("\0") !== "bunRevision\0bunVersion\0execPath" ||
2442
+ typeof report.execPath !== "string" ||
2443
+ report.execPath.length === 0 ||
2444
+ typeof report.bunVersion !== "string" ||
2445
+ typeof report.bunRevision !== "string") {
2446
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2447
+ }
2448
+ if (report.bunVersion !== BUN_TEST_ADAPTER_PROFILE.bunVersion ||
2449
+ report.bunRevision !== BUN_TEST_ADAPTER_PROFILE.bunRevision) {
2450
+ throw new Error("BUN_TEST_VERSION_UNSUPPORTED");
2451
+ }
2452
+ return {
2453
+ execPath: report.execPath,
2454
+ bunVersion: report.bunVersion,
2455
+ bunRevision: report.bunRevision,
2456
+ };
2457
+ }
2458
+ async function probeBunTestExecutable(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes) {
2459
+ const first = await runBunExecutableProbe(requestedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
2460
+ let resolvedExecutable;
2461
+ try {
2462
+ resolvedExecutable = await realpath(first.execPath);
2463
+ if (!(await stat(resolvedExecutable)).isFile())
2464
+ throw new Error("not a file");
2465
+ }
2466
+ catch {
2467
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2468
+ }
2469
+ const second = await runBunExecutableProbe(resolvedExecutable, repositoryRoot, environmentAllowlist, timeoutMs, maximumOutputBytes);
2470
+ if ((await realpath(second.execPath)) !== resolvedExecutable ||
2471
+ second.bunVersion !== first.bunVersion ||
2472
+ second.bunRevision !== first.bunRevision) {
2473
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2474
+ }
2475
+ return {
2476
+ requestedExecutable,
2477
+ resolvedExecutable,
2478
+ executableDigest: await sha256File(resolvedExecutable),
2479
+ bunVersion: second.bunVersion,
2480
+ bunRevision: second.bunRevision,
2481
+ };
2482
+ }
2182
2483
  // Evaluated by `node -p` inside a fresh container before any candidate code runs there.
2183
2484
  const CONTAINER_NODE_PROBE_SOURCE = '(() => { const fs = require("node:fs"); const crypto = require("node:crypto"); const execPath = fs.realpathSync(process.execPath); return JSON.stringify({ digest: "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(execPath)).digest("hex"), execPath, node: process.versions.node }); })()';
2184
2485
  function parseJsonBytes(bytes) {
@@ -2286,6 +2587,8 @@ function containerPreflightExecutor(container, executable) {
2286
2587
  }
2287
2588
  async function executeContainerAdapter(request, container, workspace, candidateFiles) {
2288
2589
  const adapter = request.adapter;
2590
+ if (adapter.kind === "bun-test")
2591
+ throw new Error("BUN_TEST_CONTAINER_UNSUPPORTED");
2289
2592
  const executed = await runContainerExecution(container, {
2290
2593
  workspace,
2291
2594
  ...(adapter.kind === "node-test" ? { reporterSource: NODE_TEST_REPORTER_SOURCE } : {}),
@@ -2370,6 +2673,9 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
2370
2673
  environment.TESTFORGE_RESULT_FILE = resultFile;
2371
2674
  environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
2372
2675
  }
2676
+ else if (request.adapter.kind === "bun-test") {
2677
+ environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
2678
+ }
2373
2679
  else {
2374
2680
  if (nodeTestReporterPath === undefined)
2375
2681
  throw new Error("NODE_TEST_REPORTER_MISSING");
@@ -2385,20 +2691,30 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
2385
2691
  ...request.adapter.baseTestFiles,
2386
2692
  ...candidateFiles,
2387
2693
  ]
2388
- : request.adapter.arguments;
2694
+ : request.adapter.kind === "bun-test"
2695
+ ? [
2696
+ BUN_TEST_DRIVER_PATH,
2697
+ request.adapter.executable,
2698
+ BUN_TEST_HELPER_PATH,
2699
+ ...request.adapter.baseTestFiles,
2700
+ ]
2701
+ : request.adapter.arguments;
2389
2702
  const result = await runProcess({
2390
- executable: request.adapter.executable,
2703
+ executable: request.adapter.kind === "bun-test" ? process.execPath : request.adapter.executable,
2391
2704
  args,
2392
2705
  cwd: workspace,
2393
2706
  environment,
2394
2707
  timeoutMs: request.budgets.timeoutMsPerExecution,
2395
2708
  maximumOutputBytes: request.budgets.maximumOutputBytes,
2396
2709
  });
2710
+ const structuredReport = request.adapter.kind === "testforge-command"
2711
+ ? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
2712
+ : request.adapter.kind === "bun-test"
2713
+ ? readBunDriverReport(result)
2714
+ : undefined;
2397
2715
  return {
2398
2716
  result,
2399
- structuredReport: request.adapter.kind === "testforge-command"
2400
- ? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
2401
- : undefined,
2717
+ structuredReport,
2402
2718
  nodeTestReport: request.adapter.kind === "node-test"
2403
2719
  ? await readNodeTestReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
2404
2720
  : undefined,
@@ -2444,6 +2760,119 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
2444
2760
  await removeTemporaryDirectory(temporaryRoot);
2445
2761
  }
2446
2762
  }
2763
+ async function runBunTestRuntimePreflight(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
2764
+ const probeSources = [
2765
+ {
2766
+ name: "assertion",
2767
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("assertion", () => assertSame(1, 2));\n',
2768
+ outcome: "ASSERTION_FAILURE",
2769
+ attributed: true,
2770
+ },
2771
+ {
2772
+ name: "generic-throw",
2773
+ source: 'import { test } from "bun:test"; test("generic", () => { throw new Error("generic"); });\n',
2774
+ outcome: "PROCESS_CRASH",
2775
+ attributed: false,
2776
+ },
2777
+ {
2778
+ name: "caught-assertion",
2779
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("caught", () => { try { assertSame(1, 2); } catch {} throw new Error("generic"); });\n',
2780
+ outcome: "PROCESS_CRASH",
2781
+ attributed: false,
2782
+ },
2783
+ {
2784
+ name: "replayed-assertion",
2785
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let saved; try { assertSame(1, 2); } catch (error) { saved = error; } test("replayed", () => { throw saved; });\n',
2786
+ outcome: "PROCESS_CRASH",
2787
+ attributed: false,
2788
+ },
2789
+ {
2790
+ name: "replayed-row",
2791
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let saved; test.each([0, 1])("row %i", (row) => { if (row === 0) { try { assertSame(1, 2); } catch (error) { saved = error; } return; } throw saved; });\n',
2792
+ outcome: "PROCESS_CRASH",
2793
+ attributed: false,
2794
+ },
2795
+ {
2796
+ name: "async-cross-test",
2797
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; let signalReady; const ready = new Promise((resolve) => { signalReady = resolve; }); let rejectReceiver; test("origin", () => { ready.then(() => { try { assertSame(1, 2); } catch (error) { rejectReceiver(error); } }); }); test("receiver", () => new Promise((_, reject) => { rejectReceiver = reject; signalReady(); }));\n',
2798
+ outcome: "PROCESS_CRASH",
2799
+ attributed: false,
2800
+ },
2801
+ {
2802
+ name: "multi-assertion-rows",
2803
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test.each([1, 2])("row %i", (row) => assertSame(row, 0));\n',
2804
+ outcome: "ASSERTION_FAILURE",
2805
+ attributed: true,
2806
+ },
2807
+ {
2808
+ name: "operand-throw",
2809
+ source: 'import { test } from "bun:test"; import { assertSame } from "assertledger/bun"; test("operand", () => assertSame((() => { throw new Error("operand"); })(), 1));\n',
2810
+ outcome: "PROCESS_CRASH",
2811
+ attributed: false,
2812
+ },
2813
+ {
2814
+ name: "native-expect",
2815
+ source: 'import { test, expect } from "bun:test"; test("native", () => expect(1).toBe(2));\n',
2816
+ outcome: "PROCESS_CRASH",
2817
+ attributed: false,
2818
+ },
2819
+ {
2820
+ name: "hook-failure",
2821
+ source: 'import { afterEach, test } from "bun:test"; import { assertSame } from "assertledger/bun"; afterEach(() => { throw new Error("hook"); }); test("candidate", () => assertSame(1, 2));\n',
2822
+ outcome: "INFRA_ERROR",
2823
+ attributed: false,
2824
+ },
2825
+ ];
2826
+ try {
2827
+ const probes = [];
2828
+ for (const probe of probeSources) {
2829
+ const evidence = await withEmptyWorkspace(async (workspace) => {
2830
+ await writeFile(path.join(workspace, "package.json"), '{"type":"module"}\n');
2831
+ await writeFile(path.join(workspace, "base.test.ts"), 'import { test } from "bun:test"; test("control", () => {});\n');
2832
+ const candidatePath = path.join(workspace, "candidate.test.ts");
2833
+ await writeFile(candidatePath, probe.source);
2834
+ const result = await runProcess({
2835
+ executable: process.execPath,
2836
+ args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, "base.test.ts"],
2837
+ cwd: workspace,
2838
+ environment: {
2839
+ ...environmentFromAllowlist(environmentAllowlist),
2840
+ TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
2841
+ },
2842
+ timeoutMs: Math.min(timeoutMs, 5_000),
2843
+ maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
2844
+ });
2845
+ const report = readBunDriverReport(result);
2846
+ const expectedCandidateCount = probe.name === "hook-failure"
2847
+ ? 0
2848
+ : probe.name === "replayed-row" ||
2849
+ probe.name === "async-cross-test" ||
2850
+ probe.name === "multi-assertion-rows"
2851
+ ? 2
2852
+ : 1;
2853
+ if (report?.outcome !== probe.outcome ||
2854
+ report.attributed !== probe.attributed ||
2855
+ report.testsDiscovered !==
2856
+ (expectedCandidateCount === 0 ? 0 : expectedCandidateCount + 1) ||
2857
+ report.candidateTestsDiscovered !== expectedCandidateCount) {
2858
+ throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED");
2859
+ }
2860
+ return {
2861
+ name: probe.name,
2862
+ outcome: report.outcome,
2863
+ attributed: report.attributed,
2864
+ testsDiscovered: report.testsDiscovered,
2865
+ candidateTestsDiscovered: report.candidateTestsDiscovered,
2866
+ };
2867
+ });
2868
+ probes.push(evidence);
2869
+ }
2870
+ return { protocolVersion: "1.0.0", probes };
2871
+ }
2872
+ catch (error) {
2873
+ throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: error });
2874
+ }
2875
+ }
2447
2876
  export async function verifyCampaign(value, options = {}) {
2448
2877
  const request = parseRequest(value);
2449
2878
  const container = request.isolation.kind === "container"
@@ -2466,6 +2895,15 @@ export async function verifyCampaign(value, options = {}) {
2466
2895
  if (request.adapter.kind === "node-test" && nodeIdentity !== undefined) {
2467
2896
  request.adapter.executable = nodeIdentity.resolvedExecutable;
2468
2897
  }
2898
+ const bunIdentity = request.adapter.kind === "bun-test"
2899
+ ? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
2900
+ : undefined;
2901
+ if (request.adapter.kind === "bun-test" && bunIdentity !== undefined) {
2902
+ request.adapter.executable = bunIdentity.resolvedExecutable;
2903
+ }
2904
+ const bunDriverDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
2905
+ const bunHelperDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
2906
+ const bunPreloadDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
2469
2907
  const nodeRuntimePreflight = request.adapter.kind === "node-test" && nodeIdentity !== undefined
2470
2908
  ? await runNodeTestRuntimePreflight({
2471
2909
  executable: nodeIdentity.resolvedExecutable,
@@ -2481,6 +2919,9 @@ export async function verifyCampaign(value, options = {}) {
2481
2919
  }),
2482
2920
  })
2483
2921
  : undefined;
2922
+ const bunRuntimePreflight = request.adapter.kind === "bun-test" && bunIdentity !== undefined
2923
+ ? await runBunTestRuntimePreflight(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
2924
+ : undefined;
2484
2925
  const campaignRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-campaign-")));
2485
2926
  const repositorySnapshot = path.join(campaignRoot, "repository");
2486
2927
  try {
@@ -2495,7 +2936,9 @@ export async function verifyCampaign(value, options = {}) {
2495
2936
  const inputPaths = [
2496
2937
  ...request.worlds.flatMap((world) => world.files.map((file) => file.path)),
2497
2938
  ...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
2498
- ...(request.adapter.kind === "node-test" ? request.adapter.baseTestFiles : []),
2939
+ ...(request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
2940
+ ? request.adapter.baseTestFiles
2941
+ : []),
2499
2942
  ...request.candidateRoots,
2500
2943
  ];
2501
2944
  for (const inputPath of inputPaths) {
@@ -2537,7 +2980,9 @@ export async function verifyCampaign(value, options = {}) {
2537
2980
  name: request.adapter.kind,
2538
2981
  version: request.adapter.kind === "testforge-command"
2539
2982
  ? request.adapter.protocolVersion
2540
- : (nodeIdentity?.nodeVersion ?? "unprobed"),
2983
+ : request.adapter.kind === "bun-test"
2984
+ ? (bunIdentity?.bunVersion ?? "unprobed")
2985
+ : (nodeIdentity?.nodeVersion ?? "unprobed"),
2541
2986
  configuration: request.adapter.kind === "node-test" && nodeIdentity !== undefined
2542
2987
  ? {
2543
2988
  kind: "node-test",
@@ -2560,7 +3005,31 @@ export async function verifyCampaign(value, options = {}) {
2560
3005
  ...request.adapter.baseTestFiles,
2561
3006
  ],
2562
3007
  }
2563
- : request.adapter,
3008
+ : request.adapter.kind === "bun-test" && bunIdentity !== undefined
3009
+ ? {
3010
+ kind: "bun-test",
3011
+ profile: BUN_TEST_ADAPTER_PROFILE,
3012
+ requestedExecutable: bunIdentity.requestedExecutable,
3013
+ resolvedExecutable: bunIdentity.resolvedExecutable,
3014
+ executableDigest: bunIdentity.executableDigest,
3015
+ bunVersion: bunIdentity.bunVersion,
3016
+ bunRevision: bunIdentity.bunRevision,
3017
+ driverDigest: bunDriverDigest,
3018
+ helperDigest: bunHelperDigest,
3019
+ preloadDigest: bunPreloadDigest,
3020
+ runtimePreflight: bunRuntimePreflight,
3021
+ arguments: [
3022
+ "test",
3023
+ "--max-concurrency=1",
3024
+ "--retry=0",
3025
+ "--preload",
3026
+ "./node_modules/assertledger/preload.mjs",
3027
+ "--reporter=junit",
3028
+ "--reporter-outfile=<generated-junit-file>",
3029
+ ...request.adapter.baseTestFiles,
3030
+ ],
3031
+ }
3032
+ : request.adapter,
2564
3033
  },
2565
3034
  execution: {
2566
3035
  isolation: container === undefined ? "UNSANDBOXED" : "CONTAINER",
@@ -2612,9 +3081,21 @@ export async function verifyCampaign(value, options = {}) {
2612
3081
  ? [
2613
3082
  "UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
2614
3083
  "Process-tree termination is best effort and depends on host operating-system facilities.",
3084
+ ...(request.adapter.kind === "bun-test"
3085
+ ? [
3086
+ "Only failures from the AssertLedger-owned assertSame helper are admitted as Bun assertions; native bun:test expect failures are not assertion evidence.",
3087
+ "Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
3088
+ ]
3089
+ : []),
2615
3090
  ]
2616
3091
  : [...CONTAINER_LIMITATIONS],
2617
3092
  };
3093
+ if (request.adapter.kind === "bun-test" &&
3094
+ ((await sha256File(BUN_TEST_DRIVER_PATH)) !== bunDriverDigest ||
3095
+ (await sha256File(BUN_TEST_HELPER_PATH)) !== bunHelperDigest ||
3096
+ (await sha256File(BUN_TEST_PRELOAD_PATH)) !== bunPreloadDigest)) {
3097
+ throw new Error("BUN_TEST_ASSET_CHANGED_DURING_CAMPAIGN");
3098
+ }
2618
3099
  return sealManifestArtifact(finalManifest);
2619
3100
  }
2620
3101
  finally {