jev-agent-tools 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -0
  4. package/docs/design.md +187 -0
  5. package/docs/tools/jev_ask.md +92 -0
  6. package/docs/tools/jev_ask_files.md +80 -0
  7. package/docs/tools/jev_check_diff.md +77 -0
  8. package/docs/tools/jev_find_files.md +59 -0
  9. package/docs/tools/jev_locate_in_file.md +51 -0
  10. package/docs/tools/jev_select_tests.md +59 -0
  11. package/package.json +85 -0
  12. package/rules/jev-ask.md +4 -0
  13. package/src/adapters/analysis-context.ts +100 -0
  14. package/src/adapters/ask-files.ts +236 -0
  15. package/src/adapters/ask-proof.ts +202 -0
  16. package/src/adapters/ask-syntax.ts +463 -0
  17. package/src/adapters/command.ts +240 -0
  18. package/src/adapters/docs.ts +222 -0
  19. package/src/adapters/files.ts +411 -0
  20. package/src/adapters/find.ts +151 -0
  21. package/src/adapters/git-base.ts +32 -0
  22. package/src/adapters/git-inventory.ts +94 -0
  23. package/src/adapters/git.ts +525 -0
  24. package/src/adapters/locate-file.ts +197 -0
  25. package/src/adapters/output-lines.ts +50 -0
  26. package/src/adapters/risk-callers.ts +525 -0
  27. package/src/adapters/runner-version.ts +102 -0
  28. package/src/adapters/syntax.ts +229 -0
  29. package/src/adapters/test-inventory.ts +168 -0
  30. package/src/adapters/usage.ts +21 -0
  31. package/src/adapters/utf8.ts +57 -0
  32. package/src/constants.ts +109 -0
  33. package/src/core/ask-closure.ts +419 -0
  34. package/src/core/ask-proof.ts +32 -0
  35. package/src/core/ask-references.ts +249 -0
  36. package/src/core/asks.ts +616 -0
  37. package/src/core/batches.ts +83 -0
  38. package/src/core/command-output.ts +249 -0
  39. package/src/core/diff.ts +226 -0
  40. package/src/core/docs.ts +399 -0
  41. package/src/core/find.ts +157 -0
  42. package/src/core/git.ts +5 -0
  43. package/src/core/import-boundaries.ts +102 -0
  44. package/src/core/imports.ts +691 -0
  45. package/src/core/integrity.ts +64 -0
  46. package/src/core/lexical.ts +130 -0
  47. package/src/core/locate.ts +213 -0
  48. package/src/core/output.ts +264 -0
  49. package/src/core/pointer.ts +51 -0
  50. package/src/core/risk-callers.ts +1270 -0
  51. package/src/core/runner-version.ts +66 -0
  52. package/src/core/sections.ts +269 -0
  53. package/src/core/state.ts +53 -0
  54. package/src/core/syntax.ts +8 -0
  55. package/src/core/test-commands.ts +430 -0
  56. package/src/core/test-coverage.ts +103 -0
  57. package/src/core/test-discovery.ts +1695 -0
  58. package/src/core/test-evidence.ts +649 -0
  59. package/src/core/test-state.ts +99 -0
  60. package/src/core/truncate.ts +14 -0
  61. package/src/core/units.ts +531 -0
  62. package/src/describe.ts +26 -0
  63. package/src/guide.ts +42 -0
  64. package/src/host.ts +22 -0
  65. package/src/index.ts +40 -0
  66. package/src/jev/client.ts +505 -0
  67. package/src/jev/pool.ts +60 -0
  68. package/src/jev/types.ts +60 -0
  69. package/src/presets/docs.ts +85 -0
  70. package/src/presets/risk.ts +263 -0
  71. package/src/presets/spec.ts +111 -0
  72. package/src/presets/witnesses.ts +313 -0
  73. package/src/render.ts +45 -0
  74. package/src/result.ts +4 -0
  75. package/src/run-end.ts +157 -0
  76. package/src/runtime.ts +16 -0
  77. package/src/session.ts +106 -0
  78. package/src/texts/ask-files.ts +2 -0
  79. package/src/texts/ask.ts +4 -0
  80. package/src/texts/check-diff.ts +24 -0
  81. package/src/texts/configuration.ts +2 -0
  82. package/src/texts/find.ts +14 -0
  83. package/src/texts/guide.ts +16 -0
  84. package/src/texts/locate.ts +10 -0
  85. package/src/texts/run-end.ts +13 -0
  86. package/src/texts/select-tests.ts +3 -0
  87. package/src/tools/ask-files.ts +263 -0
  88. package/src/tools/ask-schema.ts +116 -0
  89. package/src/tools/ask.ts +925 -0
  90. package/src/tools/check-diff.ts +510 -0
  91. package/src/tools/docs-check.ts +399 -0
  92. package/src/tools/find.ts +529 -0
  93. package/src/tools/locate.ts +369 -0
  94. package/src/tools/select-tests.ts +746 -0
  95. package/src/tools/spec-check.ts +210 -0
@@ -0,0 +1,430 @@
1
+ import { posix } from "node:path";
2
+ import { SELECT_FILE_SHARE } from "../constants.ts";
3
+ import { VERIFIED_NAME_FILTERS } from "./runner-version.ts";
4
+ import type { Framework, TestEntry, TestScenario } from "./test-discovery.ts";
5
+
6
+ export interface RunnerCommand {
7
+ cwd: string;
8
+ framework: Framework;
9
+ config?: string;
10
+ project?: string;
11
+ executable: string;
12
+ args: readonly string[];
13
+ files: readonly string[];
14
+ }
15
+
16
+ export interface RunnerSelection {
17
+ entry: TestEntry;
18
+ scenarioIds: readonly string[] | null;
19
+ }
20
+
21
+ export interface RunnerCommands {
22
+ commands: readonly RunnerCommand[];
23
+ limits: readonly {
24
+ files: readonly string[];
25
+ reason: string;
26
+ action: string;
27
+ }[];
28
+ }
29
+
30
+ const escapeRegex = (value: string): string =>
31
+ value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ const exactPattern = (names: readonly string[]): string =>
33
+ [...new Set(names)].map((name) => `^${escapeRegex(name)}$`).join("|");
34
+ const localPath = (entry: TestEntry, path = entry.path): string =>
35
+ posix.relative(entry.cwd, path) || ".";
36
+
37
+ function resolveSelection(selection: RunnerSelection): {
38
+ entry: TestEntry;
39
+ scenarios: readonly TestScenario[] | null;
40
+ reason?: string;
41
+ } {
42
+ const { entry, scenarioIds } = selection;
43
+ if (scenarioIds === null) return { entry, scenarios: null };
44
+ if (!entry.countKnown)
45
+ return { entry, scenarios: null, reason: "unknown scenario count" };
46
+ const ids = new Set(scenarioIds);
47
+ const scenarios = entry.scenarios.filter((scenario) => ids.has(scenario.id));
48
+ if (scenarios.length !== ids.size)
49
+ return {
50
+ entry,
51
+ scenarios: null,
52
+ reason: "unresolved scenario identifier",
53
+ };
54
+ if (scenarios.length >= entry.scenarios.length * SELECT_FILE_SHARE)
55
+ return { entry, scenarios: null };
56
+ if (entry.scenarios.some((scenario) => !scenario.reliable))
57
+ return { entry, scenarios: null, reason: "dynamic names or parameters" };
58
+ const names = new Set<string>();
59
+ for (const scenario of entry.scenarios) {
60
+ if (names.has(scenario.name))
61
+ return { entry, scenarios: null, reason: "ambiguous scenario names" };
62
+ names.add(scenario.name);
63
+ }
64
+ return { entry, scenarios };
65
+ }
66
+
67
+ /** Builds argument vectors only; neither collects nor executes any runner. */
68
+ export function buildRunnerCommands(
69
+ selected: readonly RunnerSelection[],
70
+ ): RunnerCommands {
71
+ const commands: RunnerCommand[] = [];
72
+ const limits: { files: readonly string[]; reason: string; action: string }[] =
73
+ [];
74
+ const groups = new Map<string, [RunnerSelection, ...RunnerSelection[]]>();
75
+ for (const selection of selected) {
76
+ if (selection.scenarioIds !== null && selection.scenarioIds.length === 0)
77
+ continue;
78
+ const { entry } = selection;
79
+ const key = JSON.stringify([
80
+ entry.cwd,
81
+ entry.framework,
82
+ entry.runnerVersion,
83
+ entry.config,
84
+ entry.project,
85
+ entry.options,
86
+ entry.kind,
87
+ entry.invocation,
88
+ entry.commandRefusal,
89
+ entry.nameFilterBlocked,
90
+ entry.nameFilterReason,
91
+ ]);
92
+ const group = groups.get(key);
93
+ if (group) group.push(selection);
94
+ else groups.set(key, [selection]);
95
+ }
96
+ for (const group of groups.values()) {
97
+ const entry = group[0].entry;
98
+ const files = [...new Set(group.map((item) => item.entry.path))];
99
+ const limit = (reason: string, action: string): void => {
100
+ limits.push({ files, reason, action });
101
+ };
102
+ if (entry.kind === "types") {
103
+ if (entry.invocation?.kind === "typecheck") {
104
+ commands.push({
105
+ cwd: entry.cwd,
106
+ framework: entry.framework,
107
+ ...(entry.config ? { config: entry.config } : {}),
108
+ ...(entry.project ? { project: entry.project } : {}),
109
+ executable: entry.invocation.executable,
110
+ args: [...entry.invocation.args],
111
+ files,
112
+ });
113
+ limit(
114
+ "typecheck project scope: selected files do not restrict the command",
115
+ "run the identified project type-check command without file or name filters",
116
+ );
117
+ } else {
118
+ limit(
119
+ "type tests: command not identified",
120
+ "run the project's type-check command",
121
+ );
122
+ }
123
+ continue;
124
+ }
125
+ if (entry.framework === "unknown") {
126
+ limit(
127
+ "framework unknown",
128
+ "run the listed files with the project runner",
129
+ );
130
+ continue;
131
+ }
132
+ if (entry.commandRefusal) {
133
+ limit(
134
+ entry.commandRefusal,
135
+ "run the listed files with the project runner",
136
+ );
137
+ continue;
138
+ }
139
+ if (
140
+ ["ava", "mocha"].includes(entry.framework) &&
141
+ files.some((file) => /[*?[\]{}()!]/.test(localPath(entry, file)))
142
+ ) {
143
+ limit(
144
+ "targeted path has unverified glob metacharacters",
145
+ "run the listed files with the project runner",
146
+ );
147
+ continue;
148
+ }
149
+ if (entry.invocation?.kind !== "runner")
150
+ limit(
151
+ "local_runner_unproven: native executable availability is not established",
152
+ "verify the local executable or use the project's runner script",
153
+ );
154
+ const resolved = group.map(resolveSelection);
155
+ for (const item of resolved)
156
+ if (item.entry.nameFilterReason)
157
+ limits.push({
158
+ files: [item.entry.path],
159
+ reason: `fallback: all — ${item.entry.nameFilterReason}`,
160
+ action: "run the whole file with the project runner",
161
+ });
162
+ for (const item of resolved) {
163
+ if (item.reason)
164
+ limits.push({
165
+ files: [item.entry.path],
166
+ reason: `fallback: all — ${item.reason}`,
167
+ action: "run the whole file with the project runner",
168
+ });
169
+ }
170
+ let wholeGroup = false;
171
+ const nominal = ["vitest", "jest", "node", "bun", "go", "mocha"].includes(
172
+ entry.framework,
173
+ );
174
+ if (
175
+ nominal &&
176
+ resolved.some((item) => item.scenarios === null) &&
177
+ resolved.some((item) => item.scenarios !== null)
178
+ ) {
179
+ wholeGroup = true;
180
+ limit(
181
+ "fallback: all — whole files and name filters in the same group",
182
+ "run all listed files without a name filter",
183
+ );
184
+ }
185
+ const paths = files.map((file) => localPath(entry, file));
186
+ const scenarios = resolved.flatMap((item) => item.scenarios ?? []);
187
+ let pattern =
188
+ !wholeGroup && resolved.every((item) => item.scenarios !== null)
189
+ ? exactPattern(scenarios.map((scenario) => scenario.name))
190
+ : undefined;
191
+ if (
192
+ pattern &&
193
+ (entry.framework === "vitest" ||
194
+ entry.framework === "jest" ||
195
+ entry.framework === "mocha")
196
+ ) {
197
+ const contract = VERIFIED_NAME_FILTERS[entry.framework].find(
198
+ (candidate) => candidate.version.test(entry.runnerVersion ?? ""),
199
+ );
200
+ if (
201
+ !contract ||
202
+ (contract.separator !== " " &&
203
+ scenarios.some((scenario) => !scenario.nameParts))
204
+ ) {
205
+ pattern = undefined;
206
+ limit(
207
+ `name filter skipped: ${entry.framework} ${entry.runnerVersion ?? "unknown"} not verified`,
208
+ "run the whole file with the project runner",
209
+ );
210
+ } else {
211
+ pattern = exactPattern(
212
+ scenarios.map(
213
+ (scenario) =>
214
+ scenario.nameParts?.join(contract.separator) ?? scenario.name,
215
+ ),
216
+ );
217
+ }
218
+ }
219
+ if (entry.nameFilterBlocked) pattern = undefined;
220
+ let executable: string;
221
+ let args: string[];
222
+ const config = entry.config ? localPath(entry, entry.config) : undefined;
223
+ switch (entry.framework) {
224
+ case "ava":
225
+ executable = "ava";
226
+ args = [...entry.options];
227
+ if (config && !config.endsWith("package.json"))
228
+ args.push("--config", config);
229
+ args.push(...paths);
230
+ break;
231
+ case "mocha":
232
+ executable = "mocha";
233
+ args = [...entry.options];
234
+ if (config && !config.endsWith("package.json"))
235
+ args.push("--config", config);
236
+ if (pattern) args.push("--grep", pattern);
237
+ args.push(...paths);
238
+ break;
239
+ case "deno":
240
+ executable = "deno";
241
+ args = ["test", ...entry.options];
242
+ if (config) args.push("--config", config);
243
+ args.push(...paths);
244
+ break;
245
+ case "vitest":
246
+ executable = "vitest";
247
+ args = ["run", ...entry.options];
248
+ if (config) args.push("--config", config);
249
+ if (entry.project) args.push("--project", entry.project);
250
+ if (pattern) args.push("-t", pattern);
251
+ args.push(...paths);
252
+ break;
253
+ case "jest":
254
+ executable = "jest";
255
+ args = [...entry.options];
256
+ if (config) args.push("--config", config);
257
+ if (entry.project) args.push("--selectProjects", entry.project);
258
+ if (pattern) args.push("-t", pattern);
259
+ args.push("--runTestsByPath", ...paths);
260
+ break;
261
+ case "node":
262
+ executable = "node";
263
+ if (pattern) {
264
+ if (scenarios.some((scenario) => scenario.ancestors === undefined)) {
265
+ pattern = undefined;
266
+ limit(
267
+ "fallback: all — unresolved node:test hierarchy",
268
+ "run all listed files without a name filter",
269
+ );
270
+ } else {
271
+ pattern = exactPattern(
272
+ scenarios.flatMap((scenario) => [
273
+ ...(scenario.ancestors ?? []),
274
+ scenario.name,
275
+ ]),
276
+ );
277
+ if (
278
+ scenarios.some(
279
+ (scenario) => (scenario.ancestors?.length ?? 0) > 0,
280
+ )
281
+ )
282
+ limit(
283
+ "node:test: admitting an ancestor may also run its other subtests",
284
+ "verify the expanded subtree with the project runner",
285
+ );
286
+ }
287
+ }
288
+ args = [...entry.options, "--test"];
289
+ if (pattern) args.push("--test-name-pattern", pattern);
290
+ args.push(...paths);
291
+ break;
292
+ case "bun":
293
+ executable = "bun";
294
+ args = config ? ["--config", config, "test"] : ["test"];
295
+ args.push(...entry.options);
296
+ if (pattern) args.push("--test-name-pattern", pattern);
297
+ args.push(
298
+ ...paths.map((path) => (posix.isAbsolute(path) ? path : `./${path}`)),
299
+ );
300
+ break;
301
+ case "playwright":
302
+ executable = "playwright";
303
+ args = ["test", ...entry.options];
304
+ if (config) args.push("--config", config);
305
+ if (entry.project) args.push("--project", entry.project);
306
+ for (const item of resolved) {
307
+ const path = escapeRegex(localPath(item.entry));
308
+ if (
309
+ item.scenarios === null ||
310
+ item.scenarios.some((scenario) => scenario.line < 1)
311
+ ) {
312
+ args.push(path);
313
+ if (item.scenarios !== null)
314
+ limit(
315
+ "fallback: all — unknown declaration line",
316
+ "run the whole file with the project runner",
317
+ );
318
+ } else {
319
+ args.push(
320
+ ...new Set(
321
+ item.scenarios.map((scenario) => `${path}:${scenario.line}`),
322
+ ),
323
+ );
324
+ }
325
+ }
326
+ break;
327
+ case "go": {
328
+ executable = "go";
329
+ const packages = [
330
+ ...new Set(
331
+ paths.map((path) => {
332
+ const directory = posix.dirname(path);
333
+ return directory === "."
334
+ ? "."
335
+ : directory.startsWith(".")
336
+ ? directory
337
+ : `./${directory}`;
338
+ }),
339
+ ),
340
+ ];
341
+ if (pattern) {
342
+ const names = scenarios.map((scenario) => {
343
+ const separator = scenario.name.indexOf("/");
344
+ return separator < 0
345
+ ? scenario.name
346
+ : scenario.name.slice(0, separator);
347
+ });
348
+ if (
349
+ names.some((name) => !/^(Test|Example)[A-Za-z0-9_]*$/.test(name))
350
+ ) {
351
+ pattern = undefined;
352
+ limit(
353
+ "fallback: all — benchmarks or fuzz targets cannot be expressed safely",
354
+ "run the affected packages, benchmarks and fuzz targets with the project runner",
355
+ );
356
+ } else {
357
+ pattern = exactPattern(names);
358
+ if (scenarios.some((scenario) => scenario.name.includes("/")))
359
+ limit(
360
+ "fallback: all — Go subtest: whole parent retained",
361
+ "run the selected top-level parents with the project runner",
362
+ );
363
+ }
364
+ }
365
+ args = ["test", ...entry.options, ...packages];
366
+ if (pattern) args.push("-run", pattern);
367
+ limit(
368
+ "Go runs packages, not files; examples and build constraints depend on the project",
369
+ "verify build constraints and executable examples with the project runner",
370
+ );
371
+ break;
372
+ }
373
+ case "pytest":
374
+ executable = "python";
375
+ args = ["-m", "pytest", ...entry.options];
376
+ if (config) args.push("-c", config);
377
+ for (const item of resolved) {
378
+ const path = localPath(item.entry);
379
+ if (item.scenarios === null) args.push(path);
380
+ else
381
+ for (const scenario of item.scenarios) {
382
+ const separator = scenario.name.indexOf("::");
383
+ const nodePath = scenario.name.slice(0, separator);
384
+ const isFullId =
385
+ separator >= 0 &&
386
+ (nodePath === item.entry.path || nodePath === path);
387
+ args.push(
388
+ isFullId
389
+ ? path + scenario.name.slice(separator)
390
+ : `${path}::${scenario.name}`,
391
+ );
392
+ }
393
+ }
394
+ break;
395
+ default:
396
+ continue;
397
+ }
398
+ if (entry.invocation?.kind === "runner") {
399
+ // Identified options are already part of the invocation prefix.
400
+ const bunConfig = entry.framework === "bun" && config ? 2 : 0;
401
+ const prefixLength =
402
+ entry.options.length +
403
+ bunConfig +
404
+ (entry.framework === "pytest"
405
+ ? 2
406
+ : ["jest", "ava", "mocha"].includes(entry.framework)
407
+ ? 0
408
+ : 1);
409
+ executable = entry.invocation.executable;
410
+ args = [
411
+ ...(bunConfig ? ["--config", config as string] : []),
412
+ ...(entry.framework === "vitest" && entry.invocation.args[0] !== "run"
413
+ ? ["run"]
414
+ : []),
415
+ ...entry.invocation.args,
416
+ ...args.slice(prefixLength),
417
+ ];
418
+ }
419
+ commands.push({
420
+ cwd: entry.cwd,
421
+ framework: entry.framework,
422
+ ...(entry.config ? { config: entry.config } : {}),
423
+ ...(entry.project ? { project: entry.project } : {}),
424
+ executable,
425
+ args,
426
+ files,
427
+ });
428
+ }
429
+ return { commands, limits };
430
+ }
@@ -0,0 +1,103 @@
1
+ import { posix } from "node:path";
2
+ import type { Question, State } from "../jev/types.ts";
3
+ import type { EvidenceUnit } from "./units.ts";
4
+
5
+ export interface CoverageWitnessUnit {
6
+ unit: EvidenceUnit;
7
+ label: string;
8
+ reference?: "security" | "compatibility" | "correctness";
9
+ limit: number;
10
+ evidenceOnly?: boolean;
11
+ }
12
+ export interface CoverageWitnessExpectation {
13
+ id: string;
14
+ expected: "yes" | "no";
15
+ label: string;
16
+ limit: number;
17
+ }
18
+
19
+ /** Keep synthetic execution evidence in every real residual batch. */
20
+ export function prepareCoverageWitnesses(
21
+ state: State,
22
+ questions: Record<string, Question>,
23
+ witnesses: readonly CoverageWitnessUnit[],
24
+ ): {
25
+ state: State;
26
+ questions: Record<string, Question>;
27
+ witnesses: readonly CoverageWitnessExpectation[];
28
+ } {
29
+ const file = state.testFile;
30
+ const testFile =
31
+ file && typeof file === "object" && !Array.isArray(file)
32
+ ? file
33
+ : { path: "test/preset-check.test.ts", text: "" };
34
+ const path =
35
+ typeof testFile.path === "string"
36
+ ? testFile.path
37
+ : "test/preset-check.test.ts";
38
+ const bodies: string[] = [];
39
+ const imports: string[] = [];
40
+ const scenarios = ["direct call", "renamed call", "indirect read"];
41
+ let referenceIndex = 0;
42
+ const expectations: CoverageWitnessExpectation[] = [];
43
+ const witnessQuestions: Record<string, Question> = {};
44
+ for (const witness of witnesses) {
45
+ const { unit } = witness;
46
+ if (witness.evidenceOnly) continue;
47
+ const scenario = witness.reference
48
+ ? (scenarios[referenceIndex++] ?? "direct call")
49
+ : "direct call";
50
+ if (witness.reference) {
51
+ const callable =
52
+ unit.name === "LIMIT"
53
+ ? "withinLimit"
54
+ : unit.after?.match(/export\s+function\s+([\w$]+)/)?.[1];
55
+ if (callable) {
56
+ const imported = posix.relative(posix.dirname(path), unit.file);
57
+ imports.push(
58
+ `import {${callable}} from '${imported.startsWith(".") ? imported : `./${imported}`}';`,
59
+ );
60
+ bodies.push(`test('${scenario}', () => ${callable}(2));`);
61
+ }
62
+ }
63
+ const id = `coverage_${unit.id}`;
64
+ expectations.push({
65
+ id,
66
+ expected: witness.reference ? "yes" : "no",
67
+ label: witness.label,
68
+ limit: witness.limit,
69
+ });
70
+ witnessQuestions[id] = {
71
+ type: "bool",
72
+ instructions: `When test ${scenario} runs, does the after version of unit ${unit.id} (${unit.after?.match(/export\s+function\s+([\w$]+)/)?.[1] ?? unit.name}) execute or get read, directly or through functions it calls?`,
73
+ };
74
+ }
75
+ return {
76
+ state: {
77
+ ...state,
78
+ ...(witnesses.length
79
+ ? {
80
+ testFile: {
81
+ ...testFile,
82
+ text: `${testFile.text ?? ""}\n${imports.join("\n")}\n${bodies.join("\n")}`,
83
+ },
84
+ }
85
+ : {}),
86
+ changedUnits: [
87
+ ...(Array.isArray(state.changedUnits) ? state.changedUnits : []),
88
+ ...witnesses.map(({ unit }) => ({
89
+ id: unit.id,
90
+ kind: unit.kind,
91
+ file: unit.file,
92
+ name: unit.name,
93
+ exported: unit.exported,
94
+ before: unit.before,
95
+ after: unit.after,
96
+ usedBy: [],
97
+ })),
98
+ ],
99
+ },
100
+ questions: { ...questions, ...witnessQuestions },
101
+ witnesses: expectations,
102
+ };
103
+ }