assertledger 1.4.0 → 1.5.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 (107) hide show
  1. package/README.fr.md +5 -2
  2. package/README.md +5 -2
  3. package/conformance/schema-extensions.json +15 -0
  4. package/dist/build-info.json +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +213 -8
  7. package/dist/cli.js.map +1 -1
  8. package/dist/contracts/index.d.ts +467 -4
  9. package/dist/contracts/index.d.ts.map +1 -1
  10. package/dist/contracts/index.js +256 -3
  11. package/dist/contracts/index.js.map +1 -1
  12. package/dist/contracts/qualification.d.ts +528 -0
  13. package/dist/contracts/qualification.d.ts.map +1 -0
  14. package/dist/contracts/qualification.js +357 -0
  15. package/dist/contracts/qualification.js.map +1 -0
  16. package/dist/core/index.d.ts +9 -0
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +46 -2
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/qualification.d.ts +17 -0
  21. package/dist/core/qualification.d.ts.map +1 -0
  22. package/dist/core/qualification.js +571 -0
  23. package/dist/core/qualification.js.map +1 -0
  24. package/dist/diagnostics.d.ts.map +1 -1
  25. package/dist/diagnostics.js +81 -4
  26. package/dist/diagnostics.js.map +1 -1
  27. package/dist/engine/adapters/bun-native.d.ts +35 -0
  28. package/dist/engine/adapters/bun-native.d.ts.map +1 -0
  29. package/dist/engine/adapters/bun-native.js +179 -0
  30. package/dist/engine/adapters/bun-native.js.map +1 -0
  31. package/dist/engine/adapters/bun-test-designated-profile.d.ts +23 -0
  32. package/dist/engine/adapters/bun-test-designated-profile.d.ts.map +1 -0
  33. package/dist/engine/adapters/bun-test-designated-profile.js +24 -0
  34. package/dist/engine/adapters/bun-test-designated-profile.js.map +1 -0
  35. package/dist/engine/container.d.ts +7 -0
  36. package/dist/engine/container.d.ts.map +1 -1
  37. package/dist/engine/container.js +22 -7
  38. package/dist/engine/container.js.map +1 -1
  39. package/dist/engine/git-regression.d.ts.map +1 -1
  40. package/dist/engine/git-regression.js +9 -2
  41. package/dist/engine/git-regression.js.map +1 -1
  42. package/dist/engine/index.d.ts +14 -0
  43. package/dist/engine/index.d.ts.map +1 -1
  44. package/dist/engine/index.js +828 -97
  45. package/dist/engine/index.js.map +1 -1
  46. package/dist/engine/qualification.d.ts +154 -0
  47. package/dist/engine/qualification.d.ts.map +1 -0
  48. package/dist/engine/qualification.js +725 -0
  49. package/dist/engine/qualification.js.map +1 -0
  50. package/dist/engine/test-closure.d.ts +39 -0
  51. package/dist/engine/test-closure.d.ts.map +1 -0
  52. package/dist/engine/test-closure.js +513 -0
  53. package/dist/engine/test-closure.js.map +1 -0
  54. package/dist/engine/witness-import.d.ts +31 -0
  55. package/dist/engine/witness-import.d.ts.map +1 -0
  56. package/dist/engine/witness-import.js +224 -0
  57. package/dist/engine/witness-import.js.map +1 -0
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +3 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mcp/index.d.ts.map +1 -1
  63. package/dist/mcp/index.js +65 -0
  64. package/dist/mcp/index.js.map +1 -1
  65. package/dist/sdk/index.d.ts +292 -3
  66. package/dist/sdk/index.d.ts.map +1 -1
  67. package/dist/sdk/index.js +39 -1
  68. package/dist/sdk/index.js.map +1 -1
  69. package/docs/container-isolation.md +6 -0
  70. package/docs/migration-orchestration-qualification-v1.md +109 -0
  71. package/docs/migration-verification-v4.md +94 -0
  72. package/docs/orchestration-qualification.md +167 -0
  73. package/docs/reference.md +7 -0
  74. package/docs/witness-import.md +67 -0
  75. package/examples/orchestration/README.md +68 -0
  76. package/examples/orchestration/consumer-obligations.json +101 -0
  77. package/examples/orchestration/workspace/bitbucket-pipelines.yml +10 -0
  78. package/examples/orchestration/workspace/command-shape.test.mjs +25 -0
  79. package/examples/orchestration/workspace/commands.mjs +14 -0
  80. package/examples/orchestration/workspace/completion.mjs +39 -0
  81. package/examples/orchestration/workspace/faults.json +1 -0
  82. package/examples/orchestration/workspace/native.test.ts +4 -0
  83. package/examples/orchestration/workspace/package.json +6 -0
  84. package/examples/orchestration/workspace/packages/app/package.json +12 -0
  85. package/examples/orchestration/workspace/packages/app/verdict.txt +1 -0
  86. package/examples/orchestration/workspace/packages/leaf/package.json +9 -0
  87. package/examples/orchestration/workspace/packages/leaf/verdict.txt +1 -0
  88. package/examples/orchestration/workspace/pnpm-lock.yaml +170 -0
  89. package/examples/orchestration/workspace/pnpm-workspace.yaml +2 -0
  90. package/examples/orchestration/workspace/task.mjs +23 -0
  91. package/examples/orchestration/workspace/turbo.json +38 -0
  92. package/examples/orchestration/workspace/wrapper.mjs +45 -0
  93. package/integrations/bun/driver.d.mts +30 -1
  94. package/integrations/bun/driver.mjs +215 -6
  95. package/integrations/bun/preload.mjs +186 -5
  96. package/integrations/bun-native/README.md +86 -0
  97. package/integrations/bun-native/driver.mjs +396 -0
  98. package/integrations/bun-native/preload.mjs +375 -0
  99. package/integrations/skill/SKILL.md +25 -5
  100. package/package.json +1 -1
  101. package/schemas/evidence-manifest.v4.json +995 -0
  102. package/schemas/qualification-execution-request.v1.json +571 -0
  103. package/schemas/qualification-plan.v1.json +528 -0
  104. package/schemas/qualification-receipt.v1.json +821 -0
  105. package/schemas/qualification-replay-result.v1.json +37 -0
  106. package/schemas/verification-request.v4.json +590 -0
  107. package/schemas/witness-import-request.v1.json +231 -0
@@ -1,21 +1,23 @@
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, link, 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, readlink, 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
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { createScanner, SyntaxKind } from "typescript/unstable/ast";
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";
11
+ import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVerificationRequest, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, VERIFICATION_SCHEMA_VERSION_V4, } from "../contracts/index.js";
12
12
  import { assertSafeRelativePath, canonicalize, createAgenticBenchmark, decideEvidence, portablePathKey, replayAgenticBenchmark, replayEvidenceManifest, sealManifestArtifact, sha256Canonical, } from "../core/index.js";
13
+ import { BUN_TEST_DESIGNATED_ADAPTER_PROFILE } from "./adapters/bun-test-designated-profile.js";
13
14
  import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
14
15
  import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
15
16
  import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
16
17
  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";
18
+ import { CONTAINER_ASSET_DIRECTORY, CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
18
19
  import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
20
+ import { computeTestClosure, } from "./test-closure.js";
19
21
  import { runBunRuntimeDoctorChecks, runRuntimeDoctorChecks, } from "./runtime-doctor.js";
20
22
  const DEFAULT_EXCLUDES = new Set([".git", ".testforge", "node_modules"]);
21
23
  const SHA256_PREFIX = "sha256:";
@@ -331,7 +333,9 @@ function repositoryLinkError(relative) {
331
333
  cause: relative.split(path.sep).join("/"),
332
334
  });
333
335
  }
334
- async function walkFiles(root, excludes) {
336
+ /** Walks the regular files of a repository. A link refuses the walk unless `links` collects it;
337
+ * a collected link is never followed. */
338
+ async function walkFiles(root, excludes, links) {
335
339
  const files = [];
336
340
  const spellings = new Map();
337
341
  let totalBytes = 0;
@@ -344,8 +348,13 @@ async function walkFiles(root, excludes) {
344
348
  continue;
345
349
  const relative = path.posix.join(relativeDirectory.split(path.sep).join("/"), entry.name);
346
350
  const entryStats = await lstat(path.join(root, ...relative.split("/")));
347
- if (entryStats.isSymbolicLink())
348
- throw repositoryLinkError(relative);
351
+ if (entryStats.isSymbolicLink()) {
352
+ if (links === undefined)
353
+ throw repositoryLinkError(relative);
354
+ registerPortablePath(spellings, relative);
355
+ links.push(relative);
356
+ continue;
357
+ }
349
358
  registerPortablePath(spellings, relative);
350
359
  if (entryStats.isDirectory())
351
360
  await visit(relative);
@@ -357,8 +366,176 @@ async function walkFiles(root, excludes) {
357
366
  }
358
367
  await visit("");
359
368
  files.sort(comparePortablePaths);
369
+ links?.sort(comparePortablePaths);
360
370
  return { files, spellings, totalBytes };
361
371
  }
372
+ /** Whether a link resolves, or would resolve when dangling, outside the repository root. */
373
+ async function linkEscapesRoot(root, relative) {
374
+ const linkPath = path.join(root, ...relative.split("/"));
375
+ let target;
376
+ try {
377
+ target = await realpath(linkPath);
378
+ }
379
+ catch {
380
+ try {
381
+ target = path.resolve(path.dirname(linkPath), await readlink(linkPath));
382
+ }
383
+ catch {
384
+ return true;
385
+ }
386
+ }
387
+ const fromRoot = path.relative(root, target);
388
+ return fromRoot === ".." || fromRoot.startsWith(`..${path.sep}`) || path.isAbsolute(fromRoot);
389
+ }
390
+ const LINK_DISCOVERY_ENTRY_LIMIT = 100_000;
391
+ /**
392
+ * Whether a runner discovering tests from the root could reach one through this link: the link
393
+ * is itself named as a test, or it resolves to a directory holding one. A nested link, or a tree
394
+ * past the entry limit, is assumed to reach one.
395
+ */
396
+ async function linkReachesDiscoverableTest(root, link, discoverable) {
397
+ if (discoverable(link))
398
+ return true;
399
+ let target;
400
+ try {
401
+ target = await realpath(path.join(root, ...link.split("/")));
402
+ if (!(await stat(target)).isDirectory())
403
+ return false;
404
+ }
405
+ catch {
406
+ return false;
407
+ }
408
+ let seen = 0;
409
+ const pending = [""];
410
+ for (let relative = pending.pop(); relative !== undefined; relative = pending.pop()) {
411
+ for (const entry of await readdir(path.join(target, relative), { withFileTypes: true })) {
412
+ seen += 1;
413
+ if (seen > LINK_DISCOVERY_ENTRY_LIMIT || entry.isSymbolicLink())
414
+ return true;
415
+ const child = relative === "" ? entry.name : `${relative}/${entry.name}`;
416
+ if (entry.isDirectory())
417
+ pending.push(child);
418
+ else if (discoverable(`${link}/${child}`))
419
+ return true;
420
+ }
421
+ }
422
+ return false;
423
+ }
424
+ /**
425
+ * Refuses a link that leaves the root or lies in the closure of `roots`; every other link stays
426
+ * outside the evidence. With `discoverable`, the tests a runner finds by itself, a link through
427
+ * which one can be reached is in the closure too. `strictUnbounded` also refuses any link when the
428
+ * closure is unbounded, as a campaign must; a static diagnostic reports that case without refusing.
429
+ * Without `roots` the closure is unknown and every link refuses.
430
+ */
431
+ async function assessRepositoryLinks(root, files, links, roots, read, strictUnbounded, discoverable) {
432
+ const assessment = {
433
+ links: [...links],
434
+ refused: undefined,
435
+ omitted: [],
436
+ unbounded: [],
437
+ };
438
+ if (links.length === 0)
439
+ return assessment;
440
+ for (const link of links) {
441
+ if (await linkEscapesRoot(root, link)) {
442
+ assessment.refused = { path: link, reasonCode: "REPOSITORY_LINK_ESCAPES_ROOT" };
443
+ return assessment;
444
+ }
445
+ }
446
+ if (roots === undefined) {
447
+ assessment.refused = { path: links[0], reasonCode: "TEST_CLOSURE_UNSUPPORTED" };
448
+ return assessment;
449
+ }
450
+ if (discoverable !== undefined) {
451
+ for (const link of links) {
452
+ if (await linkReachesDiscoverableTest(root, link, discoverable)) {
453
+ assessment.refused = { path: link, reasonCode: "REPOSITORY_LINK_IN_TEST_CLOSURE" };
454
+ return assessment;
455
+ }
456
+ }
457
+ }
458
+ const closure = await computeTestClosure({ files, links, roots, read });
459
+ assessment.unbounded = closure.unbounded;
460
+ const linked = closure.links[0];
461
+ if (linked !== undefined) {
462
+ assessment.refused = { path: linked, reasonCode: "REPOSITORY_LINK_IN_TEST_CLOSURE" };
463
+ return assessment;
464
+ }
465
+ if (strictUnbounded && closure.unbounded.length > 0) {
466
+ assessment.refused = { path: links[0], reasonCode: "TEST_CLOSURE_UNBOUNDED" };
467
+ return assessment;
468
+ }
469
+ assessment.omitted = [...links];
470
+ return assessment;
471
+ }
472
+ /** The digest analyzeRepository computes, over an inventory already walked. */
473
+ async function repositoryInventoryDigest(root, files) {
474
+ const digest = createHash("sha256");
475
+ for (const file of files) {
476
+ const content = await readFile(path.join(root, ...file.split("/")));
477
+ digest.update(Buffer.from(`${file}\0${content.byteLength}\0`, "utf8"));
478
+ digest.update(content);
479
+ }
480
+ return `${SHA256_PREFIX}${digest.digest("hex")}`;
481
+ }
482
+ /**
483
+ * Judges a v4 campaign's links against the module closure of every test the adapter executes,
484
+ * read with each world's and candidate's overlay. A link that leaves the root or lies in that
485
+ * closure refuses the campaign and is named; so does any link when the closure is unbounded or
486
+ * unknown, as for a structured command. The others are left out of the snapshot and recorded.
487
+ */
488
+ async function campaignOmittedLinks(request, root, files, links) {
489
+ const overlays = new Map();
490
+ for (const overlay of [
491
+ ...request.worlds.flatMap((world) => world.files),
492
+ ...request.candidates.flatMap((candidate) => candidate.files),
493
+ ]) {
494
+ const key = portablePathKey(overlay.path);
495
+ const entry = overlays.get(key) ?? { path: overlay.path, contents: [] };
496
+ entry.contents.push(overlay.content);
497
+ overlays.set(key, entry);
498
+ }
499
+ const repositoryFiles = new Set(files.map(portablePathKey));
500
+ const roots = request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
501
+ ? [
502
+ ...request.adapter.baseTestFiles,
503
+ ...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
504
+ ]
505
+ : request.adapter.kind === "bun-test-designated"
506
+ ? request.candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [candidate.designated.file])
507
+ : undefined;
508
+ const assessment = await assessRepositoryLinks(root, [
509
+ ...files,
510
+ ...[...overlays.entries()]
511
+ .filter(([key]) => !repositoryFiles.has(key))
512
+ .map(([, entry]) => entry.path),
513
+ ], links, roots, async (file) => {
514
+ const key = portablePathKey(file);
515
+ const texts = overlays.get(key)?.contents ?? [];
516
+ return repositoryFiles.has(key)
517
+ ? [await readFile(path.join(root, ...file.split("/")), "utf8"), ...texts]
518
+ : texts;
519
+ }, true);
520
+ const refused = assessment.refused;
521
+ if (refused !== undefined) {
522
+ throw new Error(refused.reasonCode === "TEST_CLOSURE_UNSUPPORTED"
523
+ ? "UNSUPPORTED_REPOSITORY_SYMLINK"
524
+ : refused.reasonCode, { cause: refused.path });
525
+ }
526
+ return assessment.omitted;
527
+ }
528
+ /** The closure roots of a test framework's discovered sources; undefined when not analyzable. */
529
+ function frameworkClosureRoots(framework, files) {
530
+ if (framework === "bun:test") {
531
+ return files.filter((file) => /(?:^|\/)[^/]+[._](?:test|spec)\.[cm]?[jt]sx?$/u.test(file));
532
+ }
533
+ if (framework === "node:test" || framework === "vitest" || framework === "jest") {
534
+ return files.filter((file) => (initEvidenceKind(file) === "TEST_SOURCE" && /\.[cm]?[jt]sx?$/u.test(file)) ||
535
+ /(?:^|\/)(?:vitest|jest)\.config\.[cm]?[jt]s$/u.test(file));
536
+ }
537
+ return undefined;
538
+ }
362
539
  function rawSha256(content) {
363
540
  return `${SHA256_PREFIX}${createHash("sha256").update(content).digest("hex")}`;
364
541
  }
@@ -727,16 +904,28 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
727
904
  const fileKey = portablePathKey(file);
728
905
  return fileKey === rootKey || fileKey.startsWith(`${rootKey}/`);
729
906
  });
730
- let inventory;
731
- try {
732
- inventory = await walkFiles(root, inventoryExcludes);
733
- }
734
- catch (error) {
735
- if (!(error instanceof Error) || error.message !== "UNSUPPORTED_REPOSITORY_SYMLINK")
736
- throw error;
737
- return initTerminalResult("CONFLICT", initEmptyDetections(["UNSUPPORTED_REPOSITORY_SYMLINK"]), [
907
+ const links = [];
908
+ const inventory = await walkFiles(root, inventoryExcludes, links);
909
+ // A link is judged against the tests that would run, once the framework is known; one that
910
+ // leaves the root refuses the repository whatever the framework.
911
+ const linkConflict = (assessment, detections) => {
912
+ options.onLinkAssessment?.(assessment);
913
+ const code = assessment.refused?.reasonCode;
914
+ const codes = [
738
915
  "UNSUPPORTED_REPOSITORY_SYMLINK",
739
- ]);
916
+ ...(code === undefined || code === "TEST_CLOSURE_UNSUPPORTED" ? [] : [code]),
917
+ ];
918
+ return initTerminalResult("CONFLICT", { ...detections, reasonCodes: codes }, codes);
919
+ };
920
+ for (const link of links) {
921
+ if (await linkEscapesRoot(root, link)) {
922
+ return linkConflict({
923
+ links: [...links],
924
+ refused: { path: link, reasonCode: "REPOSITORY_LINK_ESCAPES_ROOT" },
925
+ omitted: [],
926
+ unbounded: [],
927
+ }, initEmptyDetections([]));
928
+ }
740
929
  }
741
930
  const inScopeFiles = inventory.files.filter(outsideCandidateRoots);
742
931
  const contents = new Map();
@@ -870,6 +1059,20 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
870
1059
  detections.packageManager = packageManager;
871
1060
  return initTerminalResult("CONFLICT", detections, ["FRAMEWORK_UNDETECTED"]);
872
1061
  }
1062
+ const linkReasonCodes = [];
1063
+ if (links.length > 0) {
1064
+ const assessment = await assessRepositoryLinks(root, inScopeFiles, links, frameworkClosureRoots(framework, inScopeFiles), async (file) => [await readFile(path.join(root, ...file.split("/")), "utf8")], false, (file) => frameworkClosureRoots(framework, [file])?.length === 1);
1065
+ if (assessment.refused !== undefined) {
1066
+ const detections = initEmptyDetections([]);
1067
+ detections.packageManager = packageManager;
1068
+ detections.framework = framework;
1069
+ return linkConflict(assessment, detections);
1070
+ }
1071
+ options.onLinkAssessment?.(assessment);
1072
+ linkReasonCodes.push("REPOSITORY_LINK_OUTSIDE_TEST_CLOSURE");
1073
+ if (assessment.unbounded.length > 0)
1074
+ linkReasonCodes.push("TEST_CLOSURE_UNBOUNDED");
1075
+ }
873
1076
  let testCommand = options.testCommand === undefined ? undefined : safeInitCommand(options.testCommand);
874
1077
  if (options.testCommand !== undefined && testCommand === undefined) {
875
1078
  const detections = initEmptyDetections(["TEST_COMMAND_INVALID"]);
@@ -974,13 +1177,16 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
974
1177
  adapterRecommendation = "bun-test";
975
1178
  }
976
1179
  }
977
- const reasonCodes = adapter === undefined
978
- ? [
979
- framework === "node:test" || framework === "bun:test"
980
- ? "BASE_TEST_FILES_UNAVAILABLE"
981
- : "OFFICIAL_ADAPTER_UNAVAILABLE",
982
- ]
983
- : [];
1180
+ const reasonCodes = [
1181
+ ...(adapter === undefined
1182
+ ? [
1183
+ framework === "node:test" || framework === "bun:test"
1184
+ ? "BASE_TEST_FILES_UNAVAILABLE"
1185
+ : "OFFICIAL_ADAPTER_UNAVAILABLE",
1186
+ ]
1187
+ : []),
1188
+ ...linkReasonCodes,
1189
+ ].sort();
984
1190
  const detections = {
985
1191
  packageManager,
986
1192
  framework,
@@ -1059,14 +1265,16 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
1059
1265
  { path: INIT_LOCK_FILE, digest: rawSha256(lockContent), content: lockContent },
1060
1266
  ];
1061
1267
  let finalInScopeFiles;
1268
+ const finalLinks = [];
1062
1269
  try {
1063
- const finalInventory = await walkFiles(root, inventoryExcludes);
1270
+ const finalInventory = await walkFiles(root, inventoryExcludes, finalLinks);
1064
1271
  finalInScopeFiles = finalInventory.files.filter(outsideCandidateRoots);
1065
1272
  }
1066
1273
  catch {
1067
1274
  finalInScopeFiles = [];
1068
1275
  }
1069
- let repositoryChanged = JSON.stringify(finalInScopeFiles) !== JSON.stringify(inScopeFiles);
1276
+ let repositoryChanged = JSON.stringify(finalInScopeFiles) !== JSON.stringify(inScopeFiles) ||
1277
+ JSON.stringify(finalLinks) !== JSON.stringify(links);
1070
1278
  if (!repositoryChanged) {
1071
1279
  for (const [file, snapshot] of evidenceBytes) {
1072
1280
  try {
@@ -1130,7 +1338,7 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
1130
1338
  const result = {
1131
1339
  schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
1132
1340
  status: "UNCHANGED",
1133
- reasonCodes: [],
1341
+ reasonCodes: detections.reasonCodes,
1134
1342
  detections,
1135
1343
  actions: [],
1136
1344
  files: plannedFiles,
@@ -1151,7 +1359,8 @@ export async function initializeRepository(requestedRoot, options = {}, writeDep
1151
1359
  const resultValue = {
1152
1360
  schemaVersion: bunBuiltIn ? "2.0.0" : "1.0.0",
1153
1361
  status,
1154
- reasonCodes: [],
1362
+ // Only non-blocking link findings can remain here; the contract mirrors the detections.
1363
+ reasonCodes: detections.reasonCodes,
1155
1364
  detections,
1156
1365
  actions,
1157
1366
  files: plannedFiles,
@@ -1245,7 +1454,7 @@ export async function doctorRepositoryRuntime(requestedRoot, options) {
1245
1454
  },
1246
1455
  probeTemporaryWorkspace: probeRuntimeTemporaryWorkspace,
1247
1456
  runSyntheticPreflight: async () => {
1248
- await runBunTestRuntimePreflight(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES);
1457
+ await runBunTestRuntimePreflight(localBunDriverExecutor(resolvedExecutable, environmentAllowlist, 5_000, CONTROLLED_REPORT_MAXIMUM_BYTES));
1249
1458
  },
1250
1459
  });
1251
1460
  }
@@ -2005,6 +2214,7 @@ function parseRequest(value) {
2005
2214
  assertPortableStringCollection(candidateRoots, "DUPLICATE_CANDIDATE_ROOT");
2006
2215
  if (adapter.kind !== "node-test" &&
2007
2216
  adapter.kind !== "bun-test" &&
2217
+ adapter.kind !== "bun-test-designated" &&
2008
2218
  adapter.kind !== "testforge-command") {
2009
2219
  throw new TypeError("UNSUPPORTED_ADAPTER");
2010
2220
  }
@@ -2021,10 +2231,17 @@ function parseRequest(value) {
2021
2231
  })()
2022
2232
  : undefined;
2023
2233
  if (containerIsolation === undefined) {
2024
- if (isolation.kind !== "trusted-local")
2234
+ if (isolation.kind !== "trusted-local" && isolation.kind !== "windows-native") {
2025
2235
  throw new TypeError("UNSUPPORTED_ISOLATION");
2236
+ }
2026
2237
  if (isolation.acknowledgedUnsafeExecution !== true) {
2027
- throw new Error("UNSAFE_LOCAL_EXECUTION_NOT_ACKNOWLEDGED");
2238
+ throw new Error(isolation.kind === "windows-native"
2239
+ ? "WINDOWS_NATIVE_EXECUTION_NOT_ACKNOWLEDGED"
2240
+ : "UNSAFE_LOCAL_EXECUTION_NOT_ACKNOWLEDGED");
2241
+ }
2242
+ // The declared level names a Windows host; it is never claimed for any other platform.
2243
+ if (isolation.kind === "windows-native" && process.platform !== "win32") {
2244
+ throw new Error("WINDOWS_NATIVE_HOST_REQUIRED");
2028
2245
  }
2029
2246
  }
2030
2247
  const parsedBudgets = {
@@ -2073,6 +2290,27 @@ function parseRequest(value) {
2073
2290
  });
2074
2291
  const candidates = request.candidates.map((value) => {
2075
2292
  const candidate = requireRecord(value, "candidate");
2293
+ if (candidate.test !== undefined) {
2294
+ const test = requireRecord(candidate.test, "designated_test");
2295
+ let file;
2296
+ try {
2297
+ file = assertSafeRelativePath(requireString(test.file, "designated_test_file"), candidateRoots);
2298
+ }
2299
+ catch {
2300
+ throw new Error("FORBIDDEN_CANDIDATE_PATH");
2301
+ }
2302
+ return {
2303
+ id: requireString(candidate.id, "candidate_id"),
2304
+ files: [],
2305
+ designated: {
2306
+ file,
2307
+ path: requireStringArray(test.path, "designated_test_path"),
2308
+ expectedFailure: candidate.expectedFailure === null
2309
+ ? null
2310
+ : requireString(candidate.expectedFailure, "expected_failure"),
2311
+ },
2312
+ };
2313
+ }
2076
2314
  if (!Array.isArray(candidate.files) || candidate.files.length === 0) {
2077
2315
  throw new TypeError("INVALID_CANDIDATE_FILES");
2078
2316
  }
@@ -2138,21 +2376,41 @@ function parseRequest(value) {
2138
2376
  ...(extraArguments === undefined ? {} : { extraArguments }),
2139
2377
  };
2140
2378
  })()
2141
- : {
2142
- kind: "testforge-command",
2143
- executable: requireString(adapter.executable, "adapter_executable"),
2144
- arguments: requireStringArray(adapter.arguments, "adapter_arguments"),
2145
- protocolVersion: "1.0.0",
2146
- };
2379
+ : adapter.kind === "bun-test-designated"
2380
+ ? {
2381
+ kind: "bun-test-designated",
2382
+ executable: requireString(adapter.executable, "adapter_executable"),
2383
+ testTimeoutMs: adapter.testTimeoutMs === null || adapter.testTimeoutMs === undefined
2384
+ ? null
2385
+ : requirePositiveInteger(adapter.testTimeoutMs, "adapter_test_timeout_ms"),
2386
+ }
2387
+ : {
2388
+ kind: "testforge-command",
2389
+ executable: requireString(adapter.executable, "adapter_executable"),
2390
+ arguments: requireStringArray(adapter.arguments, "adapter_arguments"),
2391
+ protocolVersion: "1.0.0",
2392
+ };
2393
+ // A designated test runs with the same bytes in every world: no world may overlay it.
2394
+ const designatedFiles = new Set(candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [portablePathKey(candidate.designated.file)]));
2395
+ if (worlds.some((world) => world.files.some((file) => designatedFiles.has(portablePathKey(file.path))))) {
2396
+ throw new Error("DESIGNATED_TEST_OVERLAID");
2397
+ }
2398
+ const includeDependencies = repository.includeDependencies === true;
2399
+ const exclude = requireStringArray(repository.exclude, "repository_exclude");
2400
+ if (includeDependencies && exclude.some((entry) => portablePathKey(entry) === "node_modules")) {
2401
+ throw new Error("REPOSITORY_DEPENDENCIES_EXCLUDED");
2402
+ }
2147
2403
  return {
2148
2404
  schemaVersion: requireString(request.schemaVersion, "schema_version"),
2149
2405
  repository: {
2150
2406
  root: requireString(repository.root, "repository_root"),
2151
- exclude: requireStringArray(repository.exclude, "repository_exclude"),
2407
+ exclude,
2408
+ includeDependencies,
2409
+ git: repository.git === "synthesized" ? "synthesized" : "excluded",
2152
2410
  },
2153
2411
  adapter: normalizedAdapter,
2154
2412
  isolation: containerIsolation ?? {
2155
- kind: "trusted-local",
2413
+ kind: isolation.kind === "windows-native" ? "windows-native" : "trusted-local",
2156
2414
  acknowledgedUnsafeExecution: true,
2157
2415
  environmentAllowlist,
2158
2416
  },
@@ -2592,10 +2850,113 @@ function containerPreflightExecutor(container, executable) {
2592
2850
  };
2593
2851
  });
2594
2852
  }
2595
- async function executeContainerAdapter(request, container, workspace, candidateFiles) {
2853
+ const CONTAINER_BUN_PROBE_SOURCE = 'const fs = require("node:fs"); const crypto = require("node:crypto"); const execPath = fs.realpathSync(process.execPath); console.log(JSON.stringify({ digest: "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(execPath)).digest("hex"), execPath, bunVersion: Bun.version, bunRevision: Bun.revision }));';
2854
+ async function runContainerBunProbe(container, executable, workspace, timeoutMs, maximumOutputBytes) {
2855
+ const { process: result } = await runContainerExecution(container, {
2856
+ workspace,
2857
+ executable,
2858
+ args: ["-e", CONTAINER_BUN_PROBE_SOURCE],
2859
+ environment: {},
2860
+ timeoutMs: Math.min(timeoutMs, 30_000),
2861
+ maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
2862
+ }, runBoundedProcess, os.tmpdir());
2863
+ if (result.outcome !== "PASS" || result.stdout.truncated || result.stderr.totalBytes > 0) {
2864
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2865
+ }
2866
+ let report;
2867
+ try {
2868
+ report = JSON.parse(result.stdout.text.trim());
2869
+ }
2870
+ catch {
2871
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2872
+ }
2873
+ if (!isRecord(report) ||
2874
+ Object.keys(report).sort().join("\0") !== "bunRevision\0bunVersion\0digest\0execPath" ||
2875
+ typeof report.execPath !== "string" ||
2876
+ !report.execPath.startsWith("/") ||
2877
+ typeof report.digest !== "string" ||
2878
+ !/^sha256:[a-f0-9]{64}$/u.test(report.digest) ||
2879
+ typeof report.bunVersion !== "string" ||
2880
+ typeof report.bunRevision !== "string") {
2881
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2882
+ }
2883
+ if (report.bunVersion !== BUN_TEST_ADAPTER_PROFILE.bunVersion ||
2884
+ report.bunRevision !== BUN_TEST_ADAPTER_PROFILE.bunRevision) {
2885
+ throw new Error("BUN_TEST_VERSION_UNSUPPORTED");
2886
+ }
2887
+ return {
2888
+ execPath: report.execPath,
2889
+ bunVersion: report.bunVersion,
2890
+ bunRevision: report.bunRevision,
2891
+ executableDigest: report.digest,
2892
+ };
2893
+ }
2894
+ /** Resolves the image's Bun twice, as the local probe does, and binds its digest. */
2895
+ async function probeContainerBunExecutable(container, requestedExecutable, timeoutMs, maximumOutputBytes) {
2896
+ return withEmptyWorkspace(async (workspace) => {
2897
+ const first = await runContainerBunProbe(container, requestedExecutable, workspace, timeoutMs, maximumOutputBytes);
2898
+ const second = await runContainerBunProbe(container, first.execPath, workspace, timeoutMs, maximumOutputBytes);
2899
+ if (second.execPath !== first.execPath ||
2900
+ second.bunVersion !== first.bunVersion ||
2901
+ second.bunRevision !== first.bunRevision ||
2902
+ second.executableDigest !== first.executableDigest) {
2903
+ throw new Error("BUN_TEST_EXECUTABLE_PROBE_FAILED");
2904
+ }
2905
+ return {
2906
+ requestedExecutable,
2907
+ resolvedExecutable: first.execPath,
2908
+ executableDigest: first.executableDigest,
2909
+ bunVersion: first.bunVersion,
2910
+ bunRevision: first.bunRevision,
2911
+ };
2912
+ });
2913
+ }
2914
+ /** The driver, its preload and the assertion helper, as one container asset set. */
2915
+ async function bunContainerAssets() {
2916
+ return [
2917
+ { name: "driver.mjs", content: await readFile(BUN_TEST_DRIVER_PATH, "utf8") },
2918
+ { name: "preload.mjs", content: await readFile(BUN_TEST_PRELOAD_PATH, "utf8") },
2919
+ { name: "assertions.mjs", content: await readFile(BUN_TEST_HELPER_PATH, "utf8") },
2920
+ ];
2921
+ }
2922
+ /** Runs the Bun driver under the image's own Bun; the report comes back as result.json only. */
2923
+ async function runContainerBunDriver(container, workspace, executable, files, environment, timeoutMs, maximumOutputBytes) {
2924
+ const executed = await runContainerExecution(container, {
2925
+ workspace,
2926
+ assets: await bunContainerAssets(),
2927
+ executable,
2928
+ args: [
2929
+ `${CONTAINER_ASSET_DIRECTORY}/driver.mjs`,
2930
+ executable,
2931
+ `${CONTAINER_ASSET_DIRECTORY}/assertions.mjs`,
2932
+ ...files,
2933
+ ],
2934
+ environment: { ...environment, ASSERTLEDGER_BUN_RESULT_FILE: CONTAINER_RESULT_FILE },
2935
+ timeoutMs,
2936
+ maximumOutputBytes,
2937
+ resultMaximumBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
2938
+ }, runBoundedProcess, os.tmpdir());
2939
+ const result = executed.process;
2940
+ const document = result.outcome === "TIMEOUT" || result.outcome === "INFRA_ERROR"
2941
+ ? undefined
2942
+ : parseJsonBytes(executed.result);
2943
+ return { result, report: parseStructuredCommandReport(document, result) };
2944
+ }
2945
+ async function executeContainerBunAdapter(request, container, workspace, candidate, candidateFiles) {
2596
2946
  const adapter = request.adapter;
2597
- if (adapter.kind === "bun-test")
2598
- throw new Error("BUN_TEST_CONTAINER_UNSUPPORTED");
2947
+ if (adapter.kind !== "bun-test" && adapter.kind !== "bun-test-designated") {
2948
+ throw new Error("UNSUPPORTED_ADAPTER");
2949
+ }
2950
+ const { result, report } = await runContainerBunDriver(container, workspace, adapter.executable, adapter.kind === "bun-test" ? adapter.baseTestFiles : designatedFiles(request, candidate), adapter.kind === "bun-test"
2951
+ ? { TESTFORGE_CANDIDATE_FILES: JSON.stringify(candidateFiles) }
2952
+ : { ASSERTLEDGER_BUN_DESIGNATED: JSON.stringify(designatedSelection(request, candidate)) }, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
2953
+ return { result, structuredReport: report, nodeTestReport: undefined };
2954
+ }
2955
+ async function executeContainerAdapter(request, container, workspace, candidate, candidateFiles) {
2956
+ const adapter = request.adapter;
2957
+ if (adapter.kind === "bun-test" || adapter.kind === "bun-test-designated") {
2958
+ return executeContainerBunAdapter(request, container, workspace, candidate, candidateFiles);
2959
+ }
2599
2960
  const executed = await runContainerExecution(container, {
2600
2961
  workspace,
2601
2962
  ...(adapter.kind === "node-test" ? { reporterSource: NODE_TEST_REPORTER_SOURCE } : {}),
@@ -2636,7 +2997,114 @@ async function executeContainerAdapter(request, container, workspace, candidateF
2636
2997
  };
2637
2998
  }
2638
2999
  function trustedLocalEnvironmentAllowlist(request) {
2639
- return request.isolation.kind === "trusted-local" ? request.isolation.environmentAllowlist : [];
3000
+ return request.isolation.kind === "container" ? [] : request.isolation.environmentAllowlist;
3001
+ }
3002
+ /** The snapshot exclusions: the defaults and the request's names, keeping dependencies on request. */
3003
+ function campaignExcludes(request) {
3004
+ const excludes = effectiveExcludes(request.repository.exclude);
3005
+ if (request.repository.includeDependencies)
3006
+ excludes.delete(portablePathKey("node_modules"));
3007
+ return excludes;
3008
+ }
3009
+ /** A workspace copy keeps the snapshot's synthesized Git metadata; the source's own never enters. */
3010
+ function workspaceExcludes(request) {
3011
+ const excludes = campaignExcludes(request);
3012
+ if (request.repository.git === "synthesized")
3013
+ excludes.delete(portablePathKey(".git"));
3014
+ return excludes;
3015
+ }
3016
+ /**
3017
+ * Commits the snapshot once into a fresh repository with a fixed identity and date, no template,
3018
+ * hooks or global configuration, honoring the snapshot's own .gitignore. Returns the tree identity.
3019
+ */
3020
+ async function synthesizeGitWorktree(snapshot) {
3021
+ // An empty file outside the snapshot: Git for Windows cannot open os.devNull as a config file.
3022
+ const globalConfig = path.join(path.dirname(snapshot), "git-global-config");
3023
+ await writeFile(globalConfig, "", { flag: "wx" });
3024
+ const environment = {
3025
+ ...environmentFromAllowlist(["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"]),
3026
+ GIT_CONFIG_NOSYSTEM: "1",
3027
+ GIT_ATTR_NOSYSTEM: "1",
3028
+ GIT_TERMINAL_PROMPT: "0",
3029
+ GIT_CONFIG_GLOBAL: globalConfig,
3030
+ GIT_AUTHOR_NAME: "AssertLedger",
3031
+ GIT_AUTHOR_EMAIL: "snapshot@assertledger.invalid",
3032
+ GIT_AUTHOR_DATE: "1970-01-01T00:00:00Z",
3033
+ GIT_COMMITTER_NAME: "AssertLedger",
3034
+ GIT_COMMITTER_EMAIL: "snapshot@assertledger.invalid",
3035
+ GIT_COMMITTER_DATE: "1970-01-01T00:00:00Z",
3036
+ };
3037
+ const git = async (args) => {
3038
+ const result = await runProcess({
3039
+ executable: "git",
3040
+ args: [
3041
+ "-c",
3042
+ "core.autocrlf=false",
3043
+ "-c",
3044
+ "core.safecrlf=false",
3045
+ "-c",
3046
+ "core.fileMode=false",
3047
+ "-c",
3048
+ "core.hooksPath=",
3049
+ "-c",
3050
+ "commit.gpgsign=false",
3051
+ ...args,
3052
+ ],
3053
+ cwd: snapshot,
3054
+ environment,
3055
+ timeoutMs: 600_000,
3056
+ maximumOutputBytes: CONTROLLED_REPORT_MAXIMUM_BYTES,
3057
+ });
3058
+ if (result.outcome !== "PASS" || result.stdout.truncated) {
3059
+ throw new Error("REPOSITORY_GIT_SYNTHESIS_FAILED", {
3060
+ cause: result.stderr.text.slice(0, 500),
3061
+ });
3062
+ }
3063
+ return result.stdout.text.trim();
3064
+ };
3065
+ const gitVersion = await git(["--version"]);
3066
+ await git(["init", "--quiet", "--template="]);
3067
+ await git(["add", "--all"]);
3068
+ await git([
3069
+ "commit",
3070
+ "--quiet",
3071
+ "--no-verify",
3072
+ "--allow-empty",
3073
+ "--message",
3074
+ "AssertLedger snapshot",
3075
+ ]);
3076
+ const treeId = await git(["rev-parse", "HEAD^{tree}"]);
3077
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(treeId)) {
3078
+ throw new Error("REPOSITORY_GIT_SYNTHESIS_FAILED");
3079
+ }
3080
+ return { mode: "synthesized", treeId, gitVersion };
3081
+ }
3082
+ /** The selection a designated campaign gives the Bun driver: load every test, or run one. */
3083
+ function designatedSelection(request, candidate) {
3084
+ if (candidate !== null) {
3085
+ const designated = candidate.designated;
3086
+ if (designated === undefined)
3087
+ throw new Error("DESIGNATED_CANDIDATE_REQUIRED");
3088
+ return {
3089
+ mode: "run",
3090
+ test: { file: designated.file, path: designated.path },
3091
+ expectedFailure: designated.expectedFailure,
3092
+ testTimeoutMs: request.adapter.kind === "bun-test-designated" ? request.adapter.testTimeoutMs : null,
3093
+ };
3094
+ }
3095
+ return {
3096
+ mode: "load",
3097
+ tests: request.candidates.map((entry) => {
3098
+ if (entry.designated === undefined)
3099
+ throw new Error("DESIGNATED_CANDIDATE_REQUIRED");
3100
+ return { file: entry.designated.file, path: entry.designated.path };
3101
+ }),
3102
+ };
3103
+ }
3104
+ /** The test files a designated run loads: every designated file for a control, one for a run. */
3105
+ function designatedFiles(request, candidate) {
3106
+ const files = (candidate === null ? request.candidates : [candidate]).map((entry) => entry.designated?.file ?? "");
3107
+ return [...new Map(files.map((file) => [portablePathKey(file), file])).values()].sort(comparePortablePaths);
2640
3108
  }
2641
3109
  function classifyNodeTest(result, report, hasCandidate) {
2642
3110
  if (result.outcome === "TIMEOUT")
@@ -2654,7 +3122,7 @@ function classifyNodeTest(result, report, hasCandidate) {
2654
3122
  candidateFailuresAllAssertions: report?.candidateFailuresAllAssertions ?? false,
2655
3123
  });
2656
3124
  }
2657
- async function copyRepository(source, destination, excludes) {
3125
+ async function copyRepository(source, destination, excludes, omittedLinks = new Set()) {
2658
3126
  await cp(source, destination, {
2659
3127
  recursive: true,
2660
3128
  force: false,
@@ -2667,13 +3135,16 @@ async function copyRepository(source, destination, excludes) {
2667
3135
  return false;
2668
3136
  }
2669
3137
  if ((await lstat(sourcePath)).isSymbolicLink()) {
3138
+ // Only a link already judged outside every executed test's closure is left out.
3139
+ if (omittedLinks.has(portablePathKey(relative.split(path.sep).join("/"))))
3140
+ return false;
2670
3141
  throw new Error("UNSUPPORTED_REPOSITORY_SYMLINK");
2671
3142
  }
2672
3143
  return true;
2673
3144
  },
2674
3145
  });
2675
3146
  }
2676
- async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidateFiles, nodeTestReporterPath) {
3147
+ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidate, candidateFiles, nodeTestReporterPath) {
2677
3148
  const resultFile = path.join(temporaryRoot, "structured-command-result.json");
2678
3149
  const environment = environmentFromAllowlist(trustedLocalEnvironmentAllowlist(request));
2679
3150
  if (request.adapter.kind === "testforge-command") {
@@ -2683,6 +3154,9 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
2683
3154
  else if (request.adapter.kind === "bun-test") {
2684
3155
  environment.TESTFORGE_CANDIDATE_FILES = JSON.stringify(candidateFiles);
2685
3156
  }
3157
+ else if (request.adapter.kind === "bun-test-designated") {
3158
+ environment.ASSERTLEDGER_BUN_DESIGNATED = JSON.stringify(designatedSelection(request, candidate));
3159
+ }
2686
3160
  else {
2687
3161
  if (nodeTestReporterPath === undefined)
2688
3162
  throw new Error("NODE_TEST_REPORTER_MISSING");
@@ -2705,9 +3179,18 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
2705
3179
  BUN_TEST_HELPER_PATH,
2706
3180
  ...request.adapter.baseTestFiles,
2707
3181
  ]
2708
- : request.adapter.arguments;
3182
+ : request.adapter.kind === "bun-test-designated"
3183
+ ? [
3184
+ BUN_TEST_DRIVER_PATH,
3185
+ request.adapter.executable,
3186
+ BUN_TEST_HELPER_PATH,
3187
+ ...designatedFiles(request, candidate),
3188
+ ]
3189
+ : request.adapter.arguments;
2709
3190
  const result = await runProcess({
2710
- executable: request.adapter.kind === "bun-test" ? process.execPath : request.adapter.executable,
3191
+ executable: request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated"
3192
+ ? process.execPath
3193
+ : request.adapter.executable,
2711
3194
  args,
2712
3195
  cwd: workspace,
2713
3196
  environment,
@@ -2716,7 +3199,7 @@ async function executeTrustedLocalAdapter(request, workspace, temporaryRoot, can
2716
3199
  });
2717
3200
  const structuredReport = request.adapter.kind === "testforge-command"
2718
3201
  ? await readStructuredCommandReport(resultFile, result, CONTROLLED_REPORT_MAXIMUM_BYTES)
2719
- : request.adapter.kind === "bun-test"
3202
+ : request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated"
2720
3203
  ? readBunDriverReport(result)
2721
3204
  : undefined;
2722
3205
  return {
@@ -2731,14 +3214,14 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
2731
3214
  const temporaryRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-run-")));
2732
3215
  const workspace = path.join(temporaryRoot, "repository");
2733
3216
  try {
2734
- await copyRepository(repositoryRoot, workspace, effectiveExcludes(request.repository.exclude));
3217
+ await copyRepository(repositoryRoot, workspace, workspaceExcludes(request));
2735
3218
  await applyFiles(workspace, world.files);
2736
3219
  if (candidate)
2737
3220
  await applyFiles(workspace, candidate.files);
2738
3221
  const candidateFiles = candidate ? candidate.files.map((file) => file.path) : [];
2739
3222
  const { result, structuredReport, nodeTestReport } = container === undefined
2740
- ? await executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidateFiles, nodeTestReporterPath)
2741
- : await executeContainerAdapter(request, container, workspace, candidateFiles);
3223
+ ? await executeTrustedLocalAdapter(request, workspace, temporaryRoot, candidate, candidateFiles, nodeTestReporterPath)
3224
+ : await executeContainerAdapter(request, container, workspace, candidate, candidateFiles);
2742
3225
  const nodeClassification = request.adapter.kind === "node-test"
2743
3226
  ? classifyNodeTest(result, nodeTestReport, candidate !== null)
2744
3227
  : undefined;
@@ -2767,7 +3250,119 @@ async function executeObservation(request, repositoryRoot, world, candidate, att
2767
3250
  await removeTemporaryDirectory(temporaryRoot);
2768
3251
  }
2769
3252
  }
2770
- async function runBunTestRuntimePreflight(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
3253
+ function localBunDriverExecutor(executable, environmentAllowlist, timeoutMs, maximumOutputBytes) {
3254
+ return async (workspace, files, environment) => readBunDriverReport(await runProcess({
3255
+ executable: process.execPath,
3256
+ args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, ...files],
3257
+ cwd: workspace,
3258
+ environment: { ...environmentFromAllowlist(environmentAllowlist), ...environment },
3259
+ timeoutMs: Math.min(timeoutMs, 5_000),
3260
+ maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
3261
+ }));
3262
+ }
3263
+ function containerBunDriverExecutor(container, executable, timeoutMs, maximumOutputBytes) {
3264
+ return async (workspace, files, environment) => (await runContainerBunDriver(container, workspace, executable, files, environment, Math.min(timeoutMs, 30_000), Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES))).report;
3265
+ }
3266
+ const DESIGNATED_PROBES = (() => {
3267
+ const run = (name, body, outcome, expectedFailure) => {
3268
+ const counted = outcome !== "NO_TEST_DISCOVERED" && outcome !== "INFRA_ERROR";
3269
+ return {
3270
+ name,
3271
+ body,
3272
+ mode: "run",
3273
+ ...(expectedFailure === undefined ? {} : { expectedFailure }),
3274
+ outcome,
3275
+ attributed: outcome === "PASS" || outcome === "ASSERTION_FAILURE",
3276
+ testsDiscovered: counted ? 1 : 0,
3277
+ candidateTestsDiscovered: counted ? 1 : 0,
3278
+ };
3279
+ };
3280
+ const failing = 'test("probe", () => expect(1).toBe(2));';
3281
+ return [
3282
+ run("expect-assertion", failing, "ASSERTION_FAILURE"),
3283
+ run("expected-line", failing, "ASSERTION_FAILURE", "error: expect(received).toBe(expected)"),
3284
+ run("other-line", failing, "PROCESS_CRASH", "expect(received).toEqual(expected)"),
3285
+ run("assert-same", 'test("probe", () => assertSame(1, 2));', "ASSERTION_FAILURE"),
3286
+ run("async-matcher", 'test("probe", async () => { await expect(Promise.resolve(1)).resolves.toBe(2); });', "ASSERTION_FAILURE"),
3287
+ run("forged-message", 'test("probe", () => { throw new Error("expect(received).toBe(expected)"); });', "PROCESS_CRASH"),
3288
+ run("caught-expect", 'test("probe", () => { try { expect(1).toBe(2); } catch {} throw new Error("generic"); });', "PROCESS_CRASH"),
3289
+ run("replayed-expect", 'let saved; try { expect(1).toBe(2); } catch (error) { saved = error; } test("probe", () => { throw saved; });', "PROCESS_CRASH"),
3290
+ run("matcher-usage", 'test("probe", () => expect(1).toContain());', "PROCESS_CRASH"),
3291
+ run("custom-matcher", 'expect.extend({ toFail() { throw new Error("expect(received).toFail()"); } }); test("probe", () => expect(1).toFail());', "PROCESS_CRASH"),
3292
+ run("passing", 'test("probe", () => expect(1).toBe(1));', "PASS"),
3293
+ run("absent", 'test("other", () => expect(1).toBe(2));', "NO_TEST_DISCOVERED"),
3294
+ run("hook-failure", 'afterEach(() => { throw new Error("hook"); }); test("probe", () => expect(1).toBe(2));', "INFRA_ERROR"),
3295
+ {
3296
+ // The callback fails its matcher only after Bun's per-test timeout: never an assertion.
3297
+ ...run("late-after-timeout", 'test("probe", () => { const end = Date.now() + 300; while (Date.now() < end); expect(1).toBe(2); });', "INFRA_ERROR"),
3298
+ testTimeoutMs: 50,
3299
+ },
3300
+ {
3301
+ name: "load",
3302
+ body: 'test("probe", () => expect(1).toBe(2)); test("other", () => {});',
3303
+ mode: "load",
3304
+ outcome: "PASS",
3305
+ attributed: false,
3306
+ testsDiscovered: 2,
3307
+ candidateTestsDiscovered: 0,
3308
+ },
3309
+ {
3310
+ name: "load-failure",
3311
+ body: 'import "./missing-module"; test("probe", () => {});',
3312
+ mode: "load",
3313
+ outcome: "INFRA_ERROR",
3314
+ attributed: false,
3315
+ testsDiscovered: 0,
3316
+ candidateTestsDiscovered: 0,
3317
+ },
3318
+ ];
3319
+ })();
3320
+ /**
3321
+ * Qualifies the designated mode before a campaign: only a built-in matcher's or assertSame's
3322
+ * failure inside the designated test, with the declared first line, is an assertion; forged,
3323
+ * caught, replayed, misused and custom failures, hook and load failures and an absent test are not.
3324
+ */
3325
+ async function runBunDesignatedPreflight(executor) {
3326
+ const header = 'import { afterEach, expect, test } from "bun:test"; import { assertSame } from "assertledger/bun";';
3327
+ try {
3328
+ const recorded = [];
3329
+ for (const probe of DESIGNATED_PROBES) {
3330
+ const report = await withEmptyWorkspace(async (workspace) => {
3331
+ await writeFile(path.join(workspace, "package.json"), '{"type":"module"}\n');
3332
+ await writeFile(path.join(workspace, "probe.test.ts"), `${header} ${probe.body}\n`);
3333
+ const test = { file: "probe.test.ts", path: ["probe"] };
3334
+ return executor(workspace, ["probe.test.ts"], {
3335
+ ASSERTLEDGER_BUN_DESIGNATED: JSON.stringify(probe.mode === "run"
3336
+ ? {
3337
+ mode: "run",
3338
+ test,
3339
+ expectedFailure: probe.expectedFailure ?? null,
3340
+ testTimeoutMs: probe.testTimeoutMs ?? null,
3341
+ }
3342
+ : { mode: "load", tests: [test] }),
3343
+ });
3344
+ });
3345
+ if (report?.outcome !== probe.outcome ||
3346
+ report.attributed !== probe.attributed ||
3347
+ report.testsDiscovered !== probe.testsDiscovered ||
3348
+ report.candidateTestsDiscovered !== probe.candidateTestsDiscovered) {
3349
+ throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: probe.name });
3350
+ }
3351
+ recorded.push({
3352
+ name: probe.name,
3353
+ outcome: report.outcome,
3354
+ attributed: report.attributed,
3355
+ testsDiscovered: report.testsDiscovered,
3356
+ candidateTestsDiscovered: report.candidateTestsDiscovered,
3357
+ });
3358
+ }
3359
+ return { protocolVersion: "1.0.0", probes: recorded };
3360
+ }
3361
+ catch (error) {
3362
+ throw new Error("BUN_TEST_PROFILE_PREFLIGHT_FAILED", { cause: error });
3363
+ }
3364
+ }
3365
+ async function runBunTestRuntimePreflight(executor) {
2771
3366
  const probeSources = [
2772
3367
  {
2773
3368
  name: "assertion",
@@ -2838,18 +3433,9 @@ async function runBunTestRuntimePreflight(executable, environmentAllowlist, time
2838
3433
  await writeFile(path.join(workspace, "base.test.ts"), 'import { test } from "bun:test"; test("control", () => {});\n');
2839
3434
  const candidatePath = path.join(workspace, "candidate.test.ts");
2840
3435
  await writeFile(candidatePath, probe.source);
2841
- const result = await runProcess({
2842
- executable: process.execPath,
2843
- args: [BUN_TEST_DRIVER_PATH, executable, BUN_TEST_HELPER_PATH, "base.test.ts"],
2844
- cwd: workspace,
2845
- environment: {
2846
- ...environmentFromAllowlist(environmentAllowlist),
2847
- TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
2848
- },
2849
- timeoutMs: Math.min(timeoutMs, 5_000),
2850
- maximumOutputBytes: Math.min(maximumOutputBytes, CONTROLLED_REPORT_MAXIMUM_BYTES),
3436
+ const report = await executor(workspace, ["base.test.ts"], {
3437
+ TESTFORGE_CANDIDATE_FILES: JSON.stringify(["candidate.test.ts"]),
2851
3438
  });
2852
- const report = readBunDriverReport(result);
2853
3439
  const expectedCandidateCount = probe.name === "hook-failure"
2854
3440
  ? 0
2855
3441
  : probe.name === "replayed-row" ||
@@ -2886,14 +3472,20 @@ export async function verifyCampaign(value, options = {}) {
2886
3472
  ? await prepareContainerBackend(options.containerRuntime?.command ?? DEFAULT_CONTAINER_RUNTIME_COMMAND, request.isolation, runBoundedProcess, os.tmpdir())
2887
3473
  : undefined;
2888
3474
  const repositoryRoot = await resolveRepositoryRoot(request.repository.root);
2889
- const excludes = effectiveExcludes(request.repository.exclude);
2890
- const sourceInventory = await walkFiles(repositoryRoot, excludes);
3475
+ const version4 = request.schemaVersion === VERIFICATION_SCHEMA_VERSION_V4;
3476
+ const excludes = campaignExcludes(request);
3477
+ // Before v4 any link refuses the walk; v4 collects them and judges each one below.
3478
+ const sourceLinks = version4 ? [] : undefined;
3479
+ const sourceInventory = await walkFiles(repositoryRoot, excludes, sourceLinks);
2891
3480
  if (sourceInventory.files.length > request.budgets.maximumRepositoryFiles) {
2892
3481
  throw new Error("REPOSITORY_FILE_BUDGET_EXCEEDED");
2893
3482
  }
2894
3483
  if (sourceInventory.totalBytes > request.budgets.maximumRepositoryBytes) {
2895
3484
  throw new Error("REPOSITORY_BYTES_BUDGET_EXCEEDED");
2896
3485
  }
3486
+ const omittedLinks = sourceLinks === undefined || sourceLinks.length === 0
3487
+ ? []
3488
+ : await campaignOmittedLinks(request, repositoryRoot, sourceInventory.files, sourceLinks);
2897
3489
  const nodeIdentity = request.adapter.kind !== "node-test"
2898
3490
  ? undefined
2899
3491
  : container === undefined
@@ -2902,15 +3494,19 @@ export async function verifyCampaign(value, options = {}) {
2902
3494
  if (request.adapter.kind === "node-test" && nodeIdentity !== undefined) {
2903
3495
  request.adapter.executable = nodeIdentity.resolvedExecutable;
2904
3496
  }
2905
- const bunIdentity = request.adapter.kind === "bun-test"
2906
- ? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
2907
- : undefined;
2908
- if (request.adapter.kind === "bun-test" && bunIdentity !== undefined) {
3497
+ const bunAdapter = request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated";
3498
+ const bunIdentity = request.adapter.kind !== "bun-test" && request.adapter.kind !== "bun-test-designated"
3499
+ ? undefined
3500
+ : container === undefined
3501
+ ? await probeBunTestExecutable(request.adapter.executable, repositoryRoot, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
3502
+ : await probeContainerBunExecutable(container, request.adapter.executable, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
3503
+ if ((request.adapter.kind === "bun-test" || request.adapter.kind === "bun-test-designated") &&
3504
+ bunIdentity !== undefined) {
2909
3505
  request.adapter.executable = bunIdentity.resolvedExecutable;
2910
3506
  }
2911
- const bunDriverDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
2912
- const bunHelperDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
2913
- const bunPreloadDigest = request.adapter.kind === "bun-test" ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
3507
+ const bunDriverDigest = bunAdapter ? await sha256File(BUN_TEST_DRIVER_PATH) : undefined;
3508
+ const bunHelperDigest = bunAdapter ? await sha256File(BUN_TEST_HELPER_PATH) : undefined;
3509
+ const bunPreloadDigest = bunAdapter ? await sha256File(BUN_TEST_PRELOAD_PATH) : undefined;
2914
3510
  const nodeRuntimePreflight = request.adapter.kind === "node-test" && nodeIdentity !== undefined
2915
3511
  ? await runNodeTestRuntimePreflight({
2916
3512
  executable: nodeIdentity.resolvedExecutable,
@@ -2926,13 +3522,20 @@ export async function verifyCampaign(value, options = {}) {
2926
3522
  }),
2927
3523
  })
2928
3524
  : undefined;
2929
- const bunRuntimePreflight = request.adapter.kind === "bun-test" && bunIdentity !== undefined
2930
- ? await runBunTestRuntimePreflight(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
2931
- : undefined;
3525
+ const bunExecutor = bunIdentity === undefined
3526
+ ? undefined
3527
+ : container === undefined
3528
+ ? localBunDriverExecutor(bunIdentity.resolvedExecutable, trustedLocalEnvironmentAllowlist(request), request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes)
3529
+ : containerBunDriverExecutor(container, bunIdentity.resolvedExecutable, request.budgets.timeoutMsPerExecution, request.budgets.maximumOutputBytes);
3530
+ const bunRuntimePreflight = bunExecutor === undefined
3531
+ ? undefined
3532
+ : request.adapter.kind === "bun-test-designated"
3533
+ ? await runBunDesignatedPreflight(bunExecutor)
3534
+ : await runBunTestRuntimePreflight(bunExecutor);
2932
3535
  const campaignRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "testforge-campaign-")));
2933
3536
  const repositorySnapshot = path.join(campaignRoot, "repository");
2934
3537
  try {
2935
- await copyRepository(repositoryRoot, repositorySnapshot, excludes);
3538
+ await copyRepository(repositoryRoot, repositorySnapshot, excludes, new Set(omittedLinks.map(portablePathKey)));
2936
3539
  const snapshotInventory = await walkFiles(repositorySnapshot, excludes);
2937
3540
  if (snapshotInventory.files.length > request.budgets.maximumRepositoryFiles) {
2938
3541
  throw new Error("REPOSITORY_FILE_BUDGET_EXCEEDED");
@@ -2940,9 +3543,18 @@ export async function verifyCampaign(value, options = {}) {
2940
3543
  if (snapshotInventory.totalBytes > request.budgets.maximumRepositoryBytes) {
2941
3544
  throw new Error("REPOSITORY_BYTES_BUDGET_EXCEEDED");
2942
3545
  }
3546
+ const gitRecord = request.repository.git === "synthesized"
3547
+ ? await synthesizeGitWorktree(repositorySnapshot)
3548
+ : null;
3549
+ const designatedTestFiles = request.candidates.flatMap((candidate) => candidate.designated === undefined ? [] : [candidate.designated.file]);
3550
+ const snapshotFiles = new Set(snapshotInventory.files.map(portablePathKey));
3551
+ if (designatedTestFiles.some((file) => !snapshotFiles.has(portablePathKey(file)))) {
3552
+ throw new Error("DESIGNATED_TEST_FILE_MISSING");
3553
+ }
2943
3554
  const inputPaths = [
2944
3555
  ...request.worlds.flatMap((world) => world.files.map((file) => file.path)),
2945
3556
  ...request.candidates.flatMap((candidate) => candidate.files.map((file) => file.path)),
3557
+ ...designatedTestFiles,
2946
3558
  ...(request.adapter.kind === "node-test" || request.adapter.kind === "bun-test"
2947
3559
  ? request.adapter.baseTestFiles
2948
3560
  : []),
@@ -2964,7 +3576,11 @@ export async function verifyCampaign(value, options = {}) {
2964
3576
  if (nodeTestReporterPath !== undefined) {
2965
3577
  await writeFile(nodeTestReporterPath, NODE_TEST_REPORTER_SOURCE, { flag: "wx" });
2966
3578
  }
2967
- const analysis = (await analyzeRepository(repositorySnapshot));
3579
+ // A v4 digest covers exactly the snapshot, dependencies included when the request says so.
3580
+ const repositoryDigest = version4
3581
+ ? await repositoryInventoryDigest(repositorySnapshot, snapshotInventory.files)
3582
+ : (await analyzeRepository(repositorySnapshot))
3583
+ .repositoryDigest;
2968
3584
  const observations = [];
2969
3585
  for (const world of request.worlds) {
2970
3586
  for (let attempt = 1; attempt <= request.policy.requiredAttempts; attempt += 1) {
@@ -2978,16 +3594,37 @@ export async function verifyCampaign(value, options = {}) {
2978
3594
  }
2979
3595
  }
2980
3596
  }
3597
+ // A designated candidate is identified by its selection and the bytes of its test file.
3598
+ const designatedIdentities = new Map();
3599
+ for (const candidate of request.candidates) {
3600
+ if (candidate.designated === undefined)
3601
+ continue;
3602
+ const bytes = await readFile(path.join(repositorySnapshot, ...candidate.designated.file.split("/")));
3603
+ designatedIdentities.set(candidate.id, {
3604
+ digest: sha256Canonical({
3605
+ test: { file: candidate.designated.file, path: candidate.designated.path },
3606
+ expectedFailure: candidate.designated.expectedFailure,
3607
+ testFileDigest: rawSha256(bytes),
3608
+ }),
3609
+ sizeBytes: bytes.byteLength,
3610
+ });
3611
+ }
3612
+ const windowsNative = request.isolation.kind === "windows-native";
3613
+ const isolationLevel = container !== undefined
3614
+ ? "CONTAINER"
3615
+ : windowsNative
3616
+ ? "WINDOWS_NATIVE_UNSANDBOXED"
3617
+ : "UNSANDBOXED";
2981
3618
  const evidence = {
2982
3619
  schemaVersion: request.schemaVersion,
2983
- repositoryDigest: analysis.repositoryDigest,
3620
+ repositoryDigest,
2984
3621
  evidenceContext: {
2985
3622
  engine: { name: "testforge", version: "0.1.0" },
2986
3623
  adapter: {
2987
3624
  name: request.adapter.kind,
2988
3625
  version: request.adapter.kind === "testforge-command"
2989
3626
  ? request.adapter.protocolVersion
2990
- : request.adapter.kind === "bun-test"
3627
+ : bunAdapter
2991
3628
  ? (bunIdentity?.bunVersion ?? "unprobed")
2992
3629
  : (nodeIdentity?.nodeVersion ?? "unprobed"),
2993
3630
  configuration: request.adapter.kind === "node-test" && nodeIdentity !== undefined
@@ -3036,10 +3673,45 @@ export async function verifyCampaign(value, options = {}) {
3036
3673
  ...request.adapter.baseTestFiles,
3037
3674
  ],
3038
3675
  }
3039
- : request.adapter,
3676
+ : request.adapter.kind === "bun-test-designated" && bunIdentity !== undefined
3677
+ ? {
3678
+ kind: "bun-test-designated",
3679
+ profile: BUN_TEST_DESIGNATED_ADAPTER_PROFILE,
3680
+ requestedExecutable: bunIdentity.requestedExecutable,
3681
+ resolvedExecutable: bunIdentity.resolvedExecutable,
3682
+ executableDigest: bunIdentity.executableDigest,
3683
+ bunVersion: bunIdentity.bunVersion,
3684
+ bunRevision: bunIdentity.bunRevision,
3685
+ driverDigest: bunDriverDigest,
3686
+ helperDigest: bunHelperDigest,
3687
+ preloadDigest: bunPreloadDigest,
3688
+ runtimePreflight: bunRuntimePreflight,
3689
+ arguments: [
3690
+ "test",
3691
+ "--max-concurrency=1",
3692
+ "--retry=0",
3693
+ "--preload",
3694
+ "./node_modules/assertledger/preload.mjs",
3695
+ "--reporter=junit",
3696
+ "--reporter-outfile=<generated-junit-file>",
3697
+ "--test-name-pattern=<anchored-designated-test-path>",
3698
+ ...(request.adapter.testTimeoutMs === null
3699
+ ? []
3700
+ : [`--timeout=${request.adapter.testTimeoutMs}`]),
3701
+ "<designated-test-file>",
3702
+ ],
3703
+ testTimeoutMs: request.adapter.testTimeoutMs,
3704
+ designatedTests: request.candidates.map((candidate) => ({
3705
+ candidateId: candidate.id,
3706
+ file: candidate.designated?.file ?? null,
3707
+ path: candidate.designated?.path ?? [],
3708
+ expectedFailure: candidate.designated?.expectedFailure ?? null,
3709
+ })),
3710
+ }
3711
+ : request.adapter,
3040
3712
  },
3041
3713
  execution: {
3042
- isolation: container === undefined ? "UNSANDBOXED" : "CONTAINER",
3714
+ isolation: isolationLevel,
3043
3715
  environmentAllowlist: container === undefined
3044
3716
  ? trustedLocalEnvironmentAllowlist(request)
3045
3717
  : container.isolation.environment.map((variable) => variable.name).sort(),
@@ -3048,11 +3720,26 @@ export async function verifyCampaign(value, options = {}) {
3048
3720
  ...(request.schemaVersion === "1.0.0"
3049
3721
  ? {}
3050
3722
  : {
3051
- backend: container === undefined
3052
- ? { kind: "trusted-local", level: "UNSANDBOXED" }
3053
- : container.record,
3723
+ backend: container !== undefined
3724
+ ? container.record
3725
+ : windowsNative
3726
+ ? {
3727
+ kind: "windows-native",
3728
+ level: "WINDOWS_NATIVE_UNSANDBOXED",
3729
+ platform: "win32",
3730
+ }
3731
+ : { kind: "trusted-local", level: "UNSANDBOXED" },
3054
3732
  }),
3055
3733
  },
3734
+ ...(version4
3735
+ ? {
3736
+ repository: {
3737
+ includeDependencies: request.repository.includeDependencies,
3738
+ omittedLinks: [...omittedLinks].sort(),
3739
+ git: gitRecord,
3740
+ },
3741
+ }
3742
+ : {}),
3056
3743
  worlds: request.worlds.map((world) => ({
3057
3744
  id: world.id,
3058
3745
  provenance: world.provenance ?? "unspecified",
@@ -3064,29 +3751,71 @@ export async function verifyCampaign(value, options = {}) {
3064
3751
  },
3065
3752
  policy: request.policy,
3066
3753
  worlds: request.worlds.map(({ files: _files, provenance: _provenance, ...world }) => world),
3067
- candidates: request.candidates.map((candidate) => ({
3068
- id: candidate.id,
3069
- digest: sha256Canonical(candidate.files),
3070
- sizeBytes: candidate.files.reduce((total, file) => total + Buffer.byteLength(file.content), 0),
3071
- })),
3754
+ candidates: request.candidates.map((candidate) => {
3755
+ const designated = designatedIdentities.get(candidate.id);
3756
+ return {
3757
+ id: candidate.id,
3758
+ digest: designated?.digest ?? sha256Canonical(candidate.files),
3759
+ sizeBytes: designated?.sizeBytes ??
3760
+ candidate.files.reduce((total, file) => total + Buffer.byteLength(file.content), 0),
3761
+ };
3762
+ }),
3072
3763
  observations,
3073
3764
  };
3074
3765
  const manifest = decideEvidence(evidence);
3766
+ const designatedLimitations = request.adapter.kind === "bun-test-designated"
3767
+ ? [
3768
+ "Only a failure thrown inside the designated test by a bun:test built-in matcher, in Bun's matcher message form, or by assertSame is admitted as an assertion; with a declared expected line, its first message line must equal it. Custom matchers, usage errors and every other error are not assertion evidence.",
3769
+ "Controls load every designated test file without running a test: they show that each world evaluates the file and its imports and still registers the designated test, not that other tests pass.",
3770
+ "Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
3771
+ ]
3772
+ : [];
3773
+ const snapshotLimitations = version4
3774
+ ? [
3775
+ ...(request.repository.includeDependencies
3776
+ ? [
3777
+ "Dependency directories (node_modules) are part of the snapshot and its digest as found; AssertLedger did not install or verify them.",
3778
+ ]
3779
+ : []),
3780
+ ...(gitRecord !== null
3781
+ ? [
3782
+ "Each execution ran in a synthesized Git repository: the snapshot committed once with a fixed identity and date; the source repository's own Git metadata and history were not used.",
3783
+ ]
3784
+ : []),
3785
+ ...(omittedLinks.length > 0
3786
+ ? [
3787
+ `${omittedLinks.length} repository link(s) outside the module closure of every executed test were left out of the snapshot; the closure follows literal module specifiers only and does not cover files read at run time.`,
3788
+ ]
3789
+ : []),
3790
+ ]
3791
+ : [];
3075
3792
  const finalManifest = {
3076
3793
  ...manifest,
3077
3794
  adapter: request.adapter.kind === "testforge-command"
3078
3795
  ? { kind: request.adapter.kind, protocolVersion: request.adapter.protocolVersion }
3079
3796
  : { kind: request.adapter.kind },
3080
- isolation: container === undefined
3081
- ? {
3082
- kind: "trusted-local",
3083
- level: "UNSANDBOXED",
3084
- acknowledgedUnsafeExecution: true,
3085
- }
3086
- : { kind: "container", level: "CONTAINER", runtimeCommand: container.runtimeCommand },
3797
+ isolation: container !== undefined
3798
+ ? { kind: "container", level: "CONTAINER", runtimeCommand: container.runtimeCommand }
3799
+ : windowsNative
3800
+ ? {
3801
+ kind: "windows-native",
3802
+ level: "WINDOWS_NATIVE_UNSANDBOXED",
3803
+ acknowledgedUnsafeExecution: true,
3804
+ }
3805
+ : {
3806
+ kind: "trusted-local",
3807
+ level: "UNSANDBOXED",
3808
+ acknowledgedUnsafeExecution: true,
3809
+ },
3087
3810
  limitations: container === undefined
3088
3811
  ? [
3089
- "UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
3812
+ ...(windowsNative
3813
+ ? [
3814
+ "WINDOWS_NATIVE_UNSANDBOXED execution ran directly on the Windows host at the operator's explicit request; it cannot safely contain hostile code.",
3815
+ ]
3816
+ : [
3817
+ "UNSANDBOXED trusted-local execution cannot safely contain hostile candidate code.",
3818
+ ]),
3090
3819
  "Process-tree termination is best effort and depends on host operating-system facilities.",
3091
3820
  ...(request.adapter.kind === "bun-test"
3092
3821
  ? [
@@ -3094,10 +3823,12 @@ export async function verifyCampaign(value, options = {}) {
3094
3823
  "Bun callback instrumentation determines assertion ownership; JUnit counts only check completeness and never classify assertions.",
3095
3824
  ]
3096
3825
  : []),
3826
+ ...designatedLimitations,
3827
+ ...snapshotLimitations,
3097
3828
  ]
3098
- : [...CONTAINER_LIMITATIONS],
3829
+ : [...CONTAINER_LIMITATIONS, ...designatedLimitations, ...snapshotLimitations],
3099
3830
  };
3100
- if (request.adapter.kind === "bun-test" &&
3831
+ if (bunAdapter &&
3101
3832
  ((await sha256File(BUN_TEST_DRIVER_PATH)) !== bunDriverDigest ||
3102
3833
  (await sha256File(BUN_TEST_HELPER_PATH)) !== bunHelperDigest ||
3103
3834
  (await sha256File(BUN_TEST_PRELOAD_PATH)) !== bunPreloadDigest)) {