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
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@public/leaf",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "build": "bun ../../task.mjs build",
6
+ "check": "bun ../../task.mjs check",
7
+ "fail": "bun ../../task.mjs fail"
8
+ }
9
+ }
@@ -0,0 +1,170 @@
1
+ ---
2
+ lockfileVersion: '9.0'
3
+
4
+ importers:
5
+
6
+ .:
7
+ configDependencies: {}
8
+ packageManagerDependencies:
9
+ pnpm:
10
+ specifier: 12.9.1
11
+ version: 12.9.1
12
+
13
+ packages:
14
+
15
+ '@pnpm/exe.android-arm64@12.9.1':
16
+ resolution: {integrity: sha512-Hgm1XdqMTfBC8PdGLL+NjZWv4xyi7D6XNrN9fb24XGYlyTBmUtdCefKnCLmHujShRD8BEO1zqM30SNcKBDLysw==}
17
+ cpu: [arm64]
18
+ os: [android]
19
+
20
+ '@pnpm/exe.android-x64@12.9.1':
21
+ resolution: {integrity: sha512-WDGCe6jZRVZP3RC/q6hOkZylkPkIQIi1/GcrR/d/YzbihhAHXCczrk+UYnV5TgbsEnAYO7Cx+2rX3nfhHvy24w==}
22
+ cpu: [x64]
23
+ os: [android]
24
+
25
+ '@pnpm/exe.darwin-arm64@12.9.1':
26
+ resolution: {integrity: sha512-+7iiEEBgYN9szPWUnAuN6eOVMTI6Hrki5bcuC9n9xl9tTnT53Tpxpo45Hml+W4IrAU0d+Tmd0sPnVIbX59PD6w==}
27
+ cpu: [arm64]
28
+ os: [darwin]
29
+
30
+ '@pnpm/exe.darwin-x64@12.9.1':
31
+ resolution: {integrity: sha512-SK7TxJiR12GbJZGffRFHItkc+Mx+99v6xMMJ9/X//8NwWV1wo17a2bwfGI8qZpl17YwsrmPEFOOqhzE1GIlrtQ==}
32
+ cpu: [x64]
33
+ os: [darwin]
34
+
35
+ '@pnpm/exe.freebsd-x64@12.9.1':
36
+ resolution: {integrity: sha512-4AjLk6uvTs2iO6wMit2hcxJ2M32xb7OV5SNFk+w6G8/NzcjcMB7ENEpyeXyLiP0kcnp8uaCGUct2NfrLCJ35eQ==}
37
+ cpu: [x64]
38
+ os: [freebsd]
39
+
40
+ '@pnpm/exe.linux-arm64-musl@12.9.1':
41
+ resolution: {integrity: sha512-dZL2ES1ckDWM9v+EtxoOklZ0WNdtayooi9iLiDUubopudX3zXPNR2mlrxBRgjbDgECOndQen2SmyxL5eZiBuKw==}
42
+ cpu: [arm64]
43
+ os: [linux]
44
+ libc: [musl]
45
+
46
+ '@pnpm/exe.linux-arm64@12.9.1':
47
+ resolution: {integrity: sha512-cx+uF6xqwEtm7B8NV+1ve0xBg9gcuzQZ5R+0WgOWvKG/h978UljS6BgWs1ipIGuGgUdeVhm3PKmBltXIcczLXA==}
48
+ cpu: [arm64]
49
+ os: [linux]
50
+ libc: [glibc]
51
+
52
+ '@pnpm/exe.linux-ppc64@12.9.1':
53
+ resolution: {integrity: sha512-MpjPsy4XR17zAtLziQZfsN+wveWa/bUJggZtwfo3+SuyRmXnSvVkFD4+v8/3RTWyYaM6mwLUTjZDJ+g3urL+ew==}
54
+ cpu: [ppc64]
55
+ os: [linux]
56
+ libc: [glibc]
57
+
58
+ '@pnpm/exe.linux-riscv64@12.9.1':
59
+ resolution: {integrity: sha512-Oo67TKqqH+Bno+L6X7Xyorwp1QG82zKIkL0vbo1x2kcPTDiyPbdM+d6lIs4+hzeB1KHT5FDoGzZj2dad7Ft+eg==}
60
+ cpu: [riscv64]
61
+ os: [linux]
62
+ libc: [glibc]
63
+
64
+ '@pnpm/exe.linux-s390x@12.9.1':
65
+ resolution: {integrity: sha512-QAD5FRCdW36HfD58BV7DoDm6z8yv+GgSqcUV1+ERwYjpVQuG6ZTpf5o0x3KDas8eTLIW7MLYVdDdlAurWeArEg==}
66
+ cpu: [s390x]
67
+ os: [linux]
68
+ libc: [glibc]
69
+
70
+ '@pnpm/exe.linux-x64-musl@12.9.1':
71
+ resolution: {integrity: sha512-mQDhBsGqcTkrLQM4q5igmRFcr6EAl4UM/FGawY+0CWGcqPpUmYUvDy9LHy80xxkHIWVJ886gvvso2XOjfav1Jw==}
72
+ cpu: [x64]
73
+ os: [linux]
74
+ libc: [musl]
75
+
76
+ '@pnpm/exe.linux-x64@12.9.1':
77
+ resolution: {integrity: sha512-+M5N74hM4fc87AAckxx/UIW+dUnWf2eko+HchJs/NZUxehLhekNyMI/GNaNXlvd+kWR+97XXBD51XRkkNiUtrA==}
78
+ cpu: [x64]
79
+ os: [linux]
80
+ libc: [glibc]
81
+
82
+ '@pnpm/exe.win32-arm64@12.9.1':
83
+ resolution: {integrity: sha512-mI28C2wxobrDi6x1OYhBRyG10hnDq/4sEIyy5RzoX6LB4kLuv6K/d25HOpMwlW20cuM4M72k9NZpOdcmUxMbEA==}
84
+ cpu: [arm64]
85
+ os: [win32]
86
+
87
+ '@pnpm/exe.win32-x64@12.9.1':
88
+ resolution: {integrity: sha512-SWoDMT0oD7NrDSIoPpkKD09D9fAQKNjwFZ3lezlCUhAfXxMv70zsfrNT2/P+GoCK2kefXF9OysUZkH1l5vvvfA==}
89
+ cpu: [x64]
90
+ os: [win32]
91
+
92
+ pnpm@12.9.1:
93
+ resolution: {integrity: sha512-BrBV//XNINwSeB3hc87TMQzglRvy7GICEaFgRdVETVyTpmMhB2TvKaZTphBFm6A2jw50+E2AxUcjz5Wq57wNbQ==}
94
+ engines: {node: '>=18.*'}
95
+ hasBin: true
96
+
97
+ snapshots:
98
+
99
+ '@pnpm/exe.android-arm64@12.9.1':
100
+ optional: true
101
+
102
+ '@pnpm/exe.android-x64@12.9.1':
103
+ optional: true
104
+
105
+ '@pnpm/exe.darwin-arm64@12.9.1':
106
+ optional: true
107
+
108
+ '@pnpm/exe.darwin-x64@12.9.1':
109
+ optional: true
110
+
111
+ '@pnpm/exe.freebsd-x64@12.9.1':
112
+ optional: true
113
+
114
+ '@pnpm/exe.linux-arm64-musl@12.9.1':
115
+ optional: true
116
+
117
+ '@pnpm/exe.linux-arm64@12.9.1':
118
+ optional: true
119
+
120
+ '@pnpm/exe.linux-ppc64@12.9.1':
121
+ optional: true
122
+
123
+ '@pnpm/exe.linux-riscv64@12.9.1':
124
+ optional: true
125
+
126
+ '@pnpm/exe.linux-s390x@12.9.1':
127
+ optional: true
128
+
129
+ '@pnpm/exe.linux-x64-musl@12.9.1':
130
+ optional: true
131
+
132
+ '@pnpm/exe.linux-x64@12.9.1':
133
+ optional: true
134
+
135
+ '@pnpm/exe.win32-arm64@12.9.1':
136
+ optional: true
137
+
138
+ '@pnpm/exe.win32-x64@12.9.1':
139
+ optional: true
140
+
141
+ pnpm@12.9.1:
142
+ optionalDependencies:
143
+ '@pnpm/exe.android-arm64': 12.9.1
144
+ '@pnpm/exe.android-x64': 12.9.1
145
+ '@pnpm/exe.darwin-arm64': 12.9.1
146
+ '@pnpm/exe.darwin-x64': 12.9.1
147
+ '@pnpm/exe.freebsd-x64': 12.9.1
148
+ '@pnpm/exe.linux-arm64': 12.9.1
149
+ '@pnpm/exe.linux-arm64-musl': 12.9.1
150
+ '@pnpm/exe.linux-ppc64': 12.9.1
151
+ '@pnpm/exe.linux-riscv64': 12.9.1
152
+ '@pnpm/exe.linux-s390x': 12.9.1
153
+ '@pnpm/exe.linux-x64': 12.9.1
154
+ '@pnpm/exe.linux-x64-musl': 12.9.1
155
+ '@pnpm/exe.win32-arm64': 12.9.1
156
+ '@pnpm/exe.win32-x64': 12.9.1
157
+
158
+ ---
159
+ lockfileVersion: '9.0'
160
+ settings:
161
+ autoInstallPeers: true
162
+ excludeLinksFromLockfile: false
163
+ importers:
164
+ .: {}
165
+ packages/leaf: {}
166
+ packages/app:
167
+ dependencies:
168
+ '@public/leaf':
169
+ specifier: workspace:*
170
+ version: link:../leaf
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - packages/*
@@ -0,0 +1,23 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { complete } from "./completion.mjs";
3
+ const kind = process.argv[2];
4
+ const name = JSON.parse(readFileSync("package.json", "utf8")).name;
5
+ const task = `${name}#${kind}`;
6
+ const emit = (event) =>
7
+ appendFileSync(
8
+ process.env.ASSERTLEDGER_QUALIFICATION_TRACE,
9
+ `${JSON.stringify({ nonce: process.env.ASSERTLEDGER_QUALIFICATION_NONCE, task, event })}\n`,
10
+ );
11
+ emit("start");
12
+ if (kind === "fail" && name === "@public/leaf") {
13
+ emit("fail");
14
+ complete(7);
15
+ process.exit(7);
16
+ }
17
+ if (kind === "build") {
18
+ mkdirSync("dist", { recursive: true });
19
+ const verdict = readFileSync("verdict.txt", "utf8");
20
+ writeFileSync("dist/result.txt", verdict);
21
+ writeFileSync("dist/manifest.txt", "required-output\n");
22
+ }
23
+ emit("finish");
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "globalEnv": ["PATH"],
4
+ "globalPassThroughEnv": [
5
+ "ASSERTLEDGER_QUALIFICATION_TRACE",
6
+ "ASSERTLEDGER_QUALIFICATION_NONCE",
7
+ "ASSERTLEDGER_QUALIFICATION_CACHE",
8
+ "ASSERTLEDGER_QUALIFICATION_RESULT_FILE"
9
+ ],
10
+ "tasks": {
11
+ "build": {
12
+ "dependsOn": ["^build"],
13
+ "inputs": [
14
+ "$TURBO_DEFAULT$",
15
+ "$TURBO_ROOT$/task.mjs",
16
+ "$TURBO_ROOT$/completion.mjs",
17
+ "!dist/**",
18
+ "!.turbo/**",
19
+ "!node_modules/**",
20
+ "verdict.txt"
21
+ ],
22
+ "outputs": ["dist/**"]
23
+ },
24
+ "check": {
25
+ "cache": false,
26
+ "inputs": [
27
+ "$TURBO_DEFAULT$",
28
+ "$TURBO_ROOT$/task.mjs",
29
+ "$TURBO_ROOT$/completion.mjs",
30
+ "!dist/**",
31
+ "!.turbo/**",
32
+ "!node_modules/**"
33
+ ],
34
+ "outputs": []
35
+ },
36
+ "fail": { "dependsOn": ["^fail"], "cache": false, "outputs": [] }
37
+ }
38
+ }
@@ -0,0 +1,45 @@
1
+ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { complete, execute } from "./completion.mjs";
3
+ import { failCommandArgs, wrapperCommandArgs } from "./commands.mjs";
4
+ const faults = JSON.parse(readFileSync("faults.json", "utf8"));
5
+ const emit = (task, event) =>
6
+ appendFileSync(
7
+ process.env.ASSERTLEDGER_QUALIFICATION_TRACE,
8
+ `${JSON.stringify({ nonce: process.env.ASSERTLEDGER_QUALIFICATION_NONCE, task, event })}\n`,
9
+ );
10
+ const layer = process.argv[2] ?? "terminal";
11
+ emit(layer, "start");
12
+ if (layer === "seed") {
13
+ writeFileSync("success-receipt.json", JSON.stringify({ exitCode: 0 }));
14
+ emit(layer, "finish");
15
+ process.exit(0);
16
+ }
17
+ let status;
18
+ if (layer === "orchestrator") {
19
+ if (faults.omitStage || faults.staleResult) status = 0;
20
+ else {
21
+ emit("mandatory-stage", "start");
22
+ emit("mandatory-stage", "finish");
23
+ status = execute(
24
+ process.env.PUBLIC_TURBO_EXECUTABLE,
25
+ failCommandArgs({
26
+ emptySelection: faults.emptySelection,
27
+ cacheDirectory: process.env.ASSERTLEDGER_QUALIFICATION_CACHE,
28
+ }),
29
+ "leaf",
30
+ );
31
+ if (faults.swallow) status = 0;
32
+ if (faults.trailingMask) {
33
+ // A subsequent successful command replaces the failing command's status.
34
+ status = execute(process.execPath, ["-e", "process.exit(0)"], "trailing");
35
+ }
36
+ }
37
+ } else if (faults.staleResult) {
38
+ // A prior success is deliberately seeded by a separate, ordered action.
39
+ status = JSON.parse(readFileSync("success-receipt.json", "utf8")).exitCode;
40
+ } else {
41
+ status = execute(process.execPath, wrapperCommandArgs(import.meta.filename), "orchestrator");
42
+ }
43
+ emit(layer, status === 0 ? "finish" : "fail");
44
+ complete(status);
45
+ process.exit(status);
@@ -17,9 +17,38 @@ export type BunTestEvent =
17
17
  | { kind: "end"; id: string; status: "pass" | "fail"; owned: boolean }
18
18
  | { kind: "hook-error" };
19
19
 
20
+ /** The JUnit totals; `timeouts` counts the tests Bun failed with a TimeoutError. */
21
+ export interface BunJunitSummary {
22
+ tests: number;
23
+ failures: number;
24
+ skipped: number;
25
+ timeouts?: number;
26
+ }
27
+
28
+ export type BunDesignatedEvent =
29
+ | { kind: "found"; id: string; file: string; designated: boolean }
30
+ | { kind: "end"; id: string; status: "pass" | "fail"; owned: boolean }
31
+ | { kind: "registered"; designated: number; file: string }
32
+ | { kind: "hook-error" };
33
+
34
+ export function classifyBunDesignatedLoad(
35
+ events: readonly BunDesignatedEvent[] | undefined,
36
+ junit: BunJunitSummary | undefined,
37
+ designatedCount: number,
38
+ exitCode: number | null,
39
+ operationalError: boolean,
40
+ ): BunInstrumentedResult;
41
+
42
+ export function classifyBunDesignatedRun(
43
+ events: readonly BunDesignatedEvent[] | undefined,
44
+ junit: BunJunitSummary | undefined,
45
+ exitCode: number | null,
46
+ operationalError: boolean,
47
+ ): BunInstrumentedResult;
48
+
20
49
  export function classifyBunInstrumentedEvidence(
21
50
  events: readonly BunTestEvent[] | undefined,
22
- junit: { tests: number; failures: number; skipped: number } | undefined,
51
+ junit: BunJunitSummary | undefined,
23
52
  baseFiles: ReadonlySet<string>,
24
53
  candidateFiles: ReadonlySet<string>,
25
54
  exitCode: number | null,
@@ -83,10 +83,13 @@ function parseJunitSummary(xml) {
83
83
  ) {
84
84
  return undefined;
85
85
  }
86
- return { tests, failures, skipped };
86
+ // Bun reports a per-test timeout as a TimeoutError failure, even when the callback threw later.
87
+ const timeouts = [...xml.matchAll(/<failure\b[^>]*\btype="TimeoutError"/gu)].length;
88
+ if (timeouts > failures) return undefined;
89
+ return { tests, failures, skipped, timeouts };
87
90
  }
88
91
 
89
- function parseEvents(content, root, allowedFiles, evidenceKey) {
92
+ function parseEvents(content, root, allowedFiles, evidenceKey, designated = false) {
90
93
  if (!content.endsWith("\n")) return undefined;
91
94
  const lines = content.trimEnd().split("\n");
92
95
  if (lines.length === 1 && lines[0] === "") return [];
@@ -120,14 +123,26 @@ function parseEvents(content, root, allowedFiles, evidenceKey) {
120
123
  events.push({ kind: "hook-error" });
121
124
  continue;
122
125
  }
126
+ if (value.kind === "registered") {
127
+ if (!designated || Object.keys(value).sort().join(",") !== "designated,file,kind") {
128
+ return undefined;
129
+ }
130
+ const file = normalizeAbsoluteFile(value.file, root);
131
+ if (file === undefined || !allowedFiles.has(file) || !Number.isSafeInteger(value.designated))
132
+ return undefined;
133
+ events.push({ kind: "registered", file, designated: value.designated });
134
+ continue;
135
+ }
123
136
  if (typeof value.id !== "string" || !/^[0-9a-f-]{36}$/u.test(value.id)) {
124
137
  return undefined;
125
138
  }
126
139
  if (value.kind === "found") {
127
- if (Object.keys(value).sort().join(",") !== "file,id,kind") return undefined;
140
+ const expectedKeys = designated ? "designated,file,id,kind" : "file,id,kind";
141
+ if (Object.keys(value).sort().join(",") !== expectedKeys) return undefined;
142
+ if (designated && typeof value.designated !== "boolean") return undefined;
128
143
  const file = normalizeAbsoluteFile(value.file, root);
129
144
  if (file === undefined || !allowedFiles.has(file)) return undefined;
130
- events.push({ kind: "found", id: value.id, file });
145
+ events.push({ kind: "found", id: value.id, file, designated: value.designated === true });
131
146
  } else if (value.kind === "end") {
132
147
  if (value.status !== "pass" && value.status !== "fail") return undefined;
133
148
  const expectedKeys = value.status === "pass" ? "id,kind,status" : "id,kind,owned,status";
@@ -157,6 +172,8 @@ export function classifyBunInstrumentedEvidence(
157
172
  ) {
158
173
  return infrastructureFailure("INCOMPLETE_CONTROLLED_REPORT");
159
174
  }
175
+ // A test Bun timed out is never assertion evidence, whatever its callback threw afterwards.
176
+ if (junit.timeouts > 0) return infrastructureFailure("BUN_TEST_TIMEOUT");
160
177
  const found = new Map();
161
178
  const ended = new Map();
162
179
  for (const event of events) {
@@ -209,6 +226,131 @@ export function classifyBunInstrumentedEvidence(
209
226
  );
210
227
  }
211
228
 
229
+ /**
230
+ * Loads designated files without registering a test. Success proves that each file and every module
231
+ * it imports evaluate in this world and that each designated test is registered exactly once.
232
+ */
233
+ export function classifyBunDesignatedLoad(
234
+ events,
235
+ junit,
236
+ designatedCount,
237
+ exitCode,
238
+ operationalError,
239
+ ) {
240
+ if (
241
+ events === undefined ||
242
+ exitCode !== 0 ||
243
+ operationalError ||
244
+ (junit !== undefined && junit.tests !== 0) ||
245
+ events.some((event) => event.kind !== "registered")
246
+ ) {
247
+ return infrastructureFailure("DESIGNATED_LOAD_INCOMPLETE");
248
+ }
249
+ for (let index = 0; index < designatedCount; index += 1) {
250
+ const matches = events.filter((event) => event.designated === index).length;
251
+ if (matches === 0) return report("NO_TEST_DISCOVERED", events.length, 0, false);
252
+ if (matches > 1) return infrastructureFailure("DESIGNATED_TEST_AMBIGUOUS");
253
+ }
254
+ if (events.some((event) => event.designated < -1 || event.designated >= designatedCount)) {
255
+ return infrastructureFailure("DESIGNATED_TEST_INDEX_INVALID");
256
+ }
257
+ return report("PASS", events.length, 0, false);
258
+ }
259
+
260
+ /**
261
+ * Runs exactly one designated test. Only its owned assertion failure, with the expected first line
262
+ * when one is declared, is an assertion; no matching test is never a failure.
263
+ */
264
+ export function classifyBunDesignatedRun(events, junit, exitCode, operationalError) {
265
+ if (
266
+ events === undefined ||
267
+ junit === undefined ||
268
+ exitCode === null ||
269
+ operationalError ||
270
+ events.some((event) => event.kind === "hook-error" || event.kind === "registered")
271
+ ) {
272
+ return infrastructureFailure("INCOMPLETE_CONTROLLED_REPORT");
273
+ }
274
+ // A test Bun timed out is never assertion evidence, whatever its callback threw afterwards.
275
+ if (junit.timeouts > 0) return infrastructureFailure("BUN_TEST_TIMEOUT");
276
+ const found = new Map();
277
+ const ended = new Map();
278
+ for (const event of events) {
279
+ if (event.kind === "found") {
280
+ if (found.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_ID");
281
+ found.set(event.id, event);
282
+ } else {
283
+ if (!found.has(event.id)) return infrastructureFailure("UNMATCHED_TEST_END");
284
+ if (ended.has(event.id)) return infrastructureFailure("DUPLICATE_TEST_END");
285
+ ended.set(event.id, event);
286
+ }
287
+ }
288
+ if (found.size !== ended.size || found.size !== junit.tests - junit.skipped) {
289
+ return infrastructureFailure("TEST_COUNT_MISMATCH");
290
+ }
291
+ if (found.size === 0) {
292
+ return junit.failures === 0 && exitCode !== 0
293
+ ? report("NO_TEST_DISCOVERED", 0, 0, false)
294
+ : infrastructureFailure("BUN_STATUS_MISMATCH");
295
+ }
296
+ if ([...found.values()].some((event) => !event.designated)) {
297
+ return infrastructureFailure("UNEXPECTED_TEST");
298
+ }
299
+ if (found.size !== 1) return infrastructureFailure("DESIGNATED_TEST_AMBIGUOUS");
300
+ const [end] = ended.values();
301
+ const failed = end.status === "fail";
302
+ if (junit.failures !== (failed ? 1 : 0) || (exitCode === 0) === failed) {
303
+ return infrastructureFailure("BUN_STATUS_MISMATCH");
304
+ }
305
+ if (!failed) return report("PASS", 1, 1, true);
306
+ return end.owned ? report("ASSERTION_FAILURE", 1, 1, true) : report("PROCESS_CRASH", 1, 1, false);
307
+ }
308
+
309
+ function escapeRegularExpression(value) {
310
+ return value.replace(/[\\^$.*+?()[\]{}|/-]/gu, "\\$&");
311
+ }
312
+
313
+ /** Validates a designated selection against the files the driver was asked to load or run. */
314
+ function parseDesignatedSelection(value, files) {
315
+ if (value === undefined) return undefined;
316
+ const selection = JSON.parse(value);
317
+ const validTest = (test) =>
318
+ typeof test === "object" &&
319
+ test !== null &&
320
+ Object.keys(test).sort().join(",") === "file,path" &&
321
+ normalizeRelativeFile(test.file) !== undefined &&
322
+ Array.isArray(test.path) &&
323
+ test.path.length > 0 &&
324
+ test.path.every((name) => typeof name === "string" && name.length > 0);
325
+ const fileSet = (tests) => [...new Set(tests.map((test) => normalizeRelativeFile(test.file)))];
326
+ const sameFiles = (tests) =>
327
+ JSON.stringify(fileSet(tests).sort()) === JSON.stringify([...files].sort());
328
+ if (
329
+ selection?.mode === "run" &&
330
+ Object.keys(selection).sort().join(",") === "expectedFailure,mode,test,testTimeoutMs" &&
331
+ validTest(selection.test) &&
332
+ (selection.expectedFailure === null || typeof selection.expectedFailure === "string") &&
333
+ (selection.testTimeoutMs === null ||
334
+ (Number.isSafeInteger(selection.testTimeoutMs) &&
335
+ selection.testTimeoutMs >= 1 &&
336
+ selection.testTimeoutMs <= 3_600_000)) &&
337
+ sameFiles([selection.test])
338
+ ) {
339
+ return selection;
340
+ }
341
+ if (
342
+ selection?.mode === "load" &&
343
+ Object.keys(selection).sort().join(",") === "mode,tests" &&
344
+ Array.isArray(selection.tests) &&
345
+ selection.tests.length > 0 &&
346
+ selection.tests.every(validTest) &&
347
+ sameFiles(selection.tests)
348
+ ) {
349
+ return selection;
350
+ }
351
+ throw new Error("INVALID_DESIGNATED_SELECTION");
352
+ }
353
+
212
354
  async function readBoundedRegularFile(file, root) {
213
355
  const details = await lstat(file);
214
356
  if (!details.isFile() || details.size > MAX_REPORT_BYTES) throw new Error("INVALID_REPORT_FILE");
@@ -241,12 +383,13 @@ async function installHelper(helperSource, root) {
241
383
  );
242
384
  }
243
385
 
244
- async function runBun(executable, files, junitFile, root, evidenceKey) {
386
+ async function runBun(executable, files, junitFile, root, evidenceKey, extraArguments = []) {
245
387
  const exactPath = (file) => `./${file.replaceAll(path.sep, "/")}`;
246
388
  const childEnvironment = { ...process.env };
247
389
  delete childEnvironment.TESTFORGE_RESULT_FILE;
248
390
  delete childEnvironment.TESTFORGE_CANDIDATE_FILES;
249
391
  delete childEnvironment.ASSERTLEDGER_BUN_EVENTS_FILE;
392
+ delete childEnvironment.ASSERTLEDGER_BUN_RESULT_FILE;
250
393
  childEnvironment.ASSERTLEDGER_BUN_ROOT = root;
251
394
  const child = spawn(
252
395
  executable,
@@ -258,6 +401,7 @@ async function runBun(executable, files, junitFile, root, evidenceKey) {
258
401
  "./node_modules/assertledger/preload.mjs",
259
402
  "--reporter=junit",
260
403
  `--reporter-outfile=${junitFile}`,
404
+ ...extraArguments,
261
405
  ...files.map(exactPath),
262
406
  ],
263
407
  {
@@ -343,9 +487,17 @@ async function main() {
343
487
  }
344
488
  const files = [...normalizedBase, ...normalizedCandidates];
345
489
  if (new Set(files).size !== files.length) throw new Error("DUPLICATE_TEST_FILE");
490
+ const selection = parseDesignatedSelection(process.env.ASSERTLEDGER_BUN_DESIGNATED, files);
491
+ if (selection !== undefined && normalizedCandidates.length > 0) {
492
+ throw new Error("INVALID_DESIGNATED_SELECTION");
493
+ }
346
494
  await installHelper(await readFile(helperSourcePath, "utf8"), root);
347
495
  const junitFile = path.join(root, `__assertledger_bun_junit_${randomUUID()}.xml`);
348
496
  const evidenceKey = randomBytes(32);
497
+ if (selection !== undefined) {
498
+ await runDesignated(executable, baseTests, files, selection, junitFile, root, evidenceKey);
499
+ return;
500
+ }
349
501
  const execution = await runBun(
350
502
  executable,
351
503
  [...baseTests, ...candidates],
@@ -370,7 +522,64 @@ async function main() {
370
522
  } catch {
371
523
  outcome = infrastructureFailure("MISSING_OR_INVALID_REPORT");
372
524
  }
373
- process.stdout.write(`${JSON.stringify(outcome)}\n`);
525
+ await emit(outcome);
526
+ }
527
+
528
+ async function runDesignated(
529
+ executable,
530
+ testFiles,
531
+ files,
532
+ selection,
533
+ junitFile,
534
+ root,
535
+ evidenceKey,
536
+ ) {
537
+ const pattern =
538
+ selection.mode === "run"
539
+ ? [
540
+ `--test-name-pattern=^${escapeRegularExpression(selection.test.path.join(" "))}$`,
541
+ ...(selection.testTimeoutMs === null ? [] : [`--timeout=${selection.testTimeoutMs}`]),
542
+ ]
543
+ : [];
544
+ const execution = await runBun(executable, testFiles, junitFile, root, evidenceKey, pattern);
545
+ let outcome;
546
+ try {
547
+ let junit;
548
+ try {
549
+ junit = parseJunitSummary(await readBoundedRegularFile(junitFile, root));
550
+ } catch (error) {
551
+ // A load registers no test, so Bun writes no report; a run must always have one.
552
+ if (selection.mode === "run" || error?.code !== "ENOENT") throw error;
553
+ }
554
+ // No registration or no matching test leaves the signed channel empty.
555
+ const events =
556
+ execution.eventContent === undefined
557
+ ? undefined
558
+ : execution.eventContent === ""
559
+ ? []
560
+ : parseEvents(execution.eventContent, root, new Set(files), evidenceKey, true);
561
+ outcome =
562
+ selection.mode === "run"
563
+ ? classifyBunDesignatedRun(events, junit, execution.exitCode, execution.operationalError)
564
+ : classifyBunDesignatedLoad(
565
+ events,
566
+ junit,
567
+ selection.tests.length,
568
+ execution.exitCode,
569
+ execution.operationalError,
570
+ );
571
+ } catch {
572
+ outcome = infrastructureFailure("MISSING_OR_INVALID_REPORT");
573
+ }
574
+ await emit(outcome);
575
+ }
576
+
577
+ /** Prints the report and, inside a container, also writes it where the engine reads it back. */
578
+ async function emit(outcome) {
579
+ const body = `${JSON.stringify(outcome)}\n`;
580
+ const resultFile = process.env.ASSERTLEDGER_BUN_RESULT_FILE;
581
+ if (resultFile !== undefined) await writeFile(resultFile, body, { flag: "wx" });
582
+ process.stdout.write(body);
374
583
  process.exitCode = outcome.outcome === "PASS" ? 0 : 1;
375
584
  }
376
585