@jterrazz/test 15.0.0 → 15.1.1

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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as copyPlan, c as GROUND_EXPECTED, d as GROUND_SEEDS, i as match, n as Matcher, t as CaptureScope, u as GROUND_REQUESTS } from "./match.js";
2
- import { A as formatResponseDiff, B as structuralSubset, C as text, D as diffDirectories, E as DirectoryAccessor, F as mergePreservingPlaceholders, I as mergeTextPreservingPlaceholders, M as formatStdoutDiff, N as formatTableDiff, O as walkDirectory, R as renderExpected, S as TextAccessor, T as FilesystemAccessor, V as textEquals, _ as findContainersByLabel, b as TableAccessor, c as shouldUpdateSnapshots, d as BaseResult, f as validateDatabaseOption, g as resolveRoot, h as resolveCommand, i as runSpecDocument, j as formatStartupReport, k as formatDirectoryDiff, l as ContainerAccessor, m as expandWorkdir, n as SQLITE_TEMPLATE_DIR, o as ServeAdapter, p as discoverRoot, s as UPDATE_HINT, u as CliResult, v as inspectContainer, w as getCallerDir, x as JsonAccessor, y as removeContainers, z as structuralEquals } from "./artifacts.js";
2
+ import { B as text, C as FilesystemAccessor, E as walkDirectory, F as findContainersByLabel, I as inspectContainer, L as removeContainers, M as structuralEquals, N as structuralSubset, O as mergePreservingPlaceholders, P as textEquals, R as JsonAccessor, S as resolveRoot, T as diffDirectories, V as getCallerDir, _ as validateDatabaseOption, b as expandWorkdir, c as shouldUpdateSnapshots, d as formatStartupReport, f as formatStdoutDiff, g as BaseResult, h as CliResult, i as runSpecDocument, j as renderExpected, k as mergeTextPreservingPlaceholders, l as formatDirectoryDiff, m as ContainerAccessor, n as SQLITE_TEMPLATE_DIR, o as ServeAdapter, p as formatTableDiff, s as UPDATE_HINT, u as formatResponseDiff, v as TableAccessor, w as DirectoryAccessor, x as resolveCommand, y as discoverRoot, z as TextAccessor } from "./artifacts.js";
3
3
  import { a as isContract, i as defineContracts, n as contractsOf, o as isContracts, r as defineContract, t as ContractQueue } from "./queue.js";
4
4
  import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { dirname, join, relative, resolve } from "node:path";
@@ -13,25 +13,6 @@ import Database from "better-sqlite3";
13
13
  import { createHash } from "node:crypto";
14
14
  import { mockDeep } from "vitest-mock-extended";
15
15
  import MockDatePackage from "mockdate";
16
- //#region src/core/specification/shared/registry.ts
17
- let containerIntegrations = null;
18
- const composeServiceFactories = /* @__PURE__ */ new Map();
19
- /** Wire the container runtimes. Called by the package entry point. */
20
- function registerContainerIntegrations(integrations) {
21
- containerIntegrations = integrations;
22
- }
23
- /** Wire a service auto-detection factory (e.g. `'postgres'`, `'redis'`). */
24
- function registerComposeServiceFactory(type, factory) {
25
- composeServiceFactories.set(type, factory);
26
- }
27
- function getContainerIntegrations() {
28
- if (!containerIntegrations) throw new Error("@jterrazz/test: container integrations are not registered — import from '@jterrazz/test' (the package entry point wires them).");
29
- return containerIntegrations;
30
- }
31
- function getComposeServiceFactory(type) {
32
- return composeServiceFactories.get(type);
33
- }
34
- //#endregion
35
16
  //#region src/integrations/compose/compose-parser.ts
36
17
  /**
37
18
  * Parse a docker-compose file and extract service definitions.
@@ -58,7 +39,7 @@ function parseComposeFile(filePath) {
58
39
  const environment = {};
59
40
  if (def.environment) if (Array.isArray(def.environment)) for (const env of def.environment) {
60
41
  const [key, ...rest] = String(env).split("=");
61
- environment[key] = rest.join("=");
42
+ if (key !== void 0) environment[key] = rest.join("=");
62
43
  }
63
44
  else Object.assign(environment, def.environment);
64
45
  const volumes = def.volumes ? def.volumes.map((v) => String(v)) : [];
@@ -351,7 +332,7 @@ var TestcontainersAdapter = class {
351
332
  const { GenericContainer, Wait } = await import("testcontainers");
352
333
  let builder = new GenericContainer(this.image).withExposedPorts(this.containerPort);
353
334
  for (const [key, value] of Object.entries(this.env)) builder = builder.withEnvironment({ [key]: value });
354
- if (this.image.startsWith("postgres")) builder = builder.withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/, 2));
335
+ if (this.image.startsWith("postgres")) builder = builder.withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/u, 2));
355
336
  if (this.reuse) builder = builder.withReuse();
356
337
  this.container = await builder.start();
357
338
  }
@@ -375,7 +356,7 @@ var TestcontainersAdapter = class {
375
356
  async getLogs() {
376
357
  if (!this.container) return "";
377
358
  const stream = await this.container.logs();
378
- return new Promise((resolve) => {
359
+ return await new Promise((resolve) => {
379
360
  let output = "";
380
361
  stream.on("data", (chunk) => {
381
362
  output += chunk.toString();
@@ -390,6 +371,25 @@ var TestcontainersAdapter = class {
390
371
  }
391
372
  };
392
373
  //#endregion
374
+ //#region src/specification/facets/_common/registry.ts
375
+ let containerIntegrations = null;
376
+ const composeServiceFactories = /* @__PURE__ */ new Map();
377
+ /** Wire the container runtimes. Called by the package entry point. */
378
+ function registerContainerIntegrations(integrations) {
379
+ containerIntegrations = integrations;
380
+ }
381
+ /** Wire a service auto-detection factory (e.g. `'postgres'`, `'redis'`). */
382
+ function registerComposeServiceFactory(type, factory) {
383
+ composeServiceFactories.set(type, factory);
384
+ }
385
+ function getContainerIntegrations() {
386
+ if (!containerIntegrations) throw new Error("@jterrazz/test: container integrations are not registered — import from '@jterrazz/test' (the package entry point wires them).");
387
+ return containerIntegrations;
388
+ }
389
+ function getComposeServiceFactory(type) {
390
+ return composeServiceFactories.get(type);
391
+ }
392
+ //#endregion
393
393
  //#region src/integrations/hono/hono.adapter.ts
394
394
  /**
395
395
  * Server adapter that dispatches requests in-process through a Hono app instance.
@@ -423,17 +423,48 @@ var HonoAdapter = class {
423
423
  }
424
424
  };
425
425
  //#endregion
426
- //#region src/core/http-files/http-file.ts
426
+ //#region src/specification/facets/_common/result/response.ts
427
+ /**
428
+ * Read-only accessor for an HTTP response.
429
+ *
430
+ * Assertions go through `expect()`:
431
+ * `expect(result.response).toMatch('created.http')` compares status,
432
+ * a subset of headers, and the JSON body against `_expected/<name>` —
433
+ * with `{{placeholder}}` support in all three.
434
+ */
435
+ var ResponseAccessor = class {
436
+ /** The parsed JSON response body (null when parsing failed). */
437
+ body;
438
+ /** @internal Ref-capture scope shared by the current spec execution. */
439
+ captures;
440
+ /** Response headers as a flat, lower-cased key-value map. */
441
+ headers;
442
+ /** The HTTP response status code. */
443
+ status;
444
+ /** @internal Test-file directory — fixture resolution root for matchers. */
445
+ testDir;
446
+ constructor(response, testDir, captures) {
447
+ this.body = response.body;
448
+ this.headers = response.headers;
449
+ this.status = response.status;
450
+ this.testDir = testDir;
451
+ this.captures = captures ?? new CaptureScope();
452
+ }
453
+ };
454
+ //#endregion
455
+ //#region src/specification/http-files/http-file.ts
427
456
  function splitSections(content) {
428
- const lines = content.split(/\r?\n/);
457
+ const lines = content.split(/\r?\n/u);
429
458
  let index = 0;
430
- while (index < lines.length && lines[index].trim() === "") index++;
459
+ while (lines[index]?.trim() === "") index++;
431
460
  const startLine = lines[index] ?? "";
432
461
  index++;
433
462
  const headerLines = [];
434
- while (index < lines.length && lines[index].trim() !== "") {
435
- headerLines.push(lines[index]);
463
+ let header = lines[index];
464
+ while (header !== void 0 && header.trim() !== "") {
465
+ headerLines.push(header);
436
466
  index++;
467
+ header = lines[index];
437
468
  }
438
469
  return {
439
470
  body: lines.slice(index + 1).join("\n").trim(),
@@ -453,20 +484,22 @@ function parseHeaders(headerLines, fileName) {
453
484
  /** Parse a `_requests/*.http` file. First line must be `METHOD /path`. */
454
485
  function parseRequestFile(content, fileName) {
455
486
  const { body, headerLines, startLine } = splitSections(content);
456
- const start = /^(?<method>[A-Z]+)\s+(?<path>\S+)$/.exec(startLine.trim());
457
- if (!start?.groups) throw new Error(`${fileName}: first line must be "METHOD /path" (e.g. "POST /users"), got "${startLine}"`);
487
+ const start = /^(?<method>[A-Z]+)\s+(?<path>\S+)$/u.exec(startLine.trim());
488
+ const method = start?.groups?.method;
489
+ const path = start?.groups?.path;
490
+ if (method === void 0 || path === void 0) throw new Error(`${fileName}: first line must be "METHOD /path" (e.g. "POST /users"), got "${startLine}"`);
458
491
  return {
459
492
  body: body.length > 0 ? body : void 0,
460
493
  headers: parseHeaders(headerLines, fileName),
461
- method: start.groups.method,
462
- path: start.groups.path
494
+ method,
495
+ path
463
496
  };
464
497
  }
465
498
  /** Parse a response `.http` file. First line must be `HTTP/1.1 <status>`. */
466
499
  function parseResponseFile(content, fileName) {
467
500
  const { body, headerLines, startLine } = splitSections(content);
468
- const start = /^HTTP\/1\.1\s+(?<status>\S+)(?:\s+.*)?$/.exec(startLine.trim());
469
- if (!start?.groups) throw new Error(`${fileName}: first line must be "HTTP/1.1 <status>" (e.g. "HTTP/1.1 200 OK"), got "${startLine}"`);
501
+ const status = /^HTTP\/1\.1\s+(?<status>\S+)(?:\s+.*)?$/u.exec(startLine.trim())?.groups?.status;
502
+ if (status === void 0) throw new Error(`${fileName}: first line must be "HTTP/1.1 <status>" (e.g. "HTTP/1.1 200 OK"), got "${startLine}"`);
470
503
  let parsedBody;
471
504
  if (body.length > 0) try {
472
505
  parsedBody = JSON.parse(body);
@@ -477,7 +510,7 @@ function parseResponseFile(content, fileName) {
477
510
  body: parsedBody,
478
511
  hasBody: body.length > 0,
479
512
  headers: parseHeaders(headerLines, fileName),
480
- status: start.groups.status
513
+ status
481
514
  };
482
515
  }
483
516
  const STATUS_TEXT = {
@@ -505,10 +538,7 @@ function serializeResponseFile(response) {
505
538
  const statusText = STATUS_TEXT[statusNumber];
506
539
  const lines = [`HTTP/1.1 ${response.status}${statusText ? ` ${statusText}` : ""}`];
507
540
  for (const [name, value] of Object.entries(response.headers)) lines.push(`${name}: ${value}`);
508
- if (response.hasBody) {
509
- lines.push("");
510
- lines.push(typeof response.body === "string" && !isJsonLike(response.body) ? response.body : JSON.stringify(response.body, null, 4));
511
- }
541
+ if (response.hasBody) lines.push("", typeof response.body === "string" && !isJsonLike(response.body) ? response.body : JSON.stringify(response.body, null, 4));
512
542
  return `${lines.join("\n")}\n`;
513
543
  }
514
544
  function isJsonLike(text) {
@@ -520,35 +550,6 @@ function isJsonLike(text) {
520
550
  }
521
551
  }
522
552
  //#endregion
523
- //#region src/core/specification/shared/result/response.ts
524
- /**
525
- * Read-only accessor for an HTTP response.
526
- *
527
- * Assertions go through `expect()`:
528
- * `expect(result.response).toMatch('created.http')` compares status,
529
- * a subset of headers, and the JSON body against `_expected/<name>` —
530
- * with `{{placeholder}}` support in all three.
531
- */
532
- var ResponseAccessor = class {
533
- /** The parsed JSON response body (null when parsing failed). */
534
- body;
535
- /** @internal Ref-capture scope shared by the current spec execution. */
536
- captures;
537
- /** Response headers as a flat, lower-cased key-value map. */
538
- headers;
539
- /** The HTTP response status code. */
540
- status;
541
- /** @internal Test-file directory — fixture resolution root for matchers. */
542
- testDir;
543
- constructor(response, testDir, captures) {
544
- this.body = response.body;
545
- this.headers = response.headers;
546
- this.status = response.status;
547
- this.testDir = testDir;
548
- this.captures = captures ?? new CaptureScope();
549
- }
550
- };
551
- //#endregion
552
553
  //#region src/vitest/matchers.ts
553
554
  /**
554
555
  * Vitest custom matchers for `@jterrazz/test` accessors.
@@ -570,7 +571,7 @@ const FAIL = (message) => ({
570
571
  pass: false
571
572
  });
572
573
  function requireExtension(name, subject) {
573
- if (!/\.[A-Za-z0-9]+$/.test(name)) throw new Error(`toMatch("${name}"): the extension is part of the name and is required for ${subject} subjects (e.g. "help.txt").`);
574
+ if (!/\.[A-Za-z0-9]+$/u.test(name)) throw new Error(`toMatch("${name}"): the extension is part of the name and is required for ${subject} subjects (e.g. "help.txt").`);
574
575
  }
575
576
  function formatJson(value) {
576
577
  return `${JSON.stringify(value, null, 4)}\n`;
@@ -639,7 +640,7 @@ function buildUpdatedResponse(previous, actual, workdir) {
639
640
  * @internal Exported for unit tests.
640
641
  */
641
642
  function compareResponse(name, expected, actual, scope) {
642
- if (!(/^\d+$/.test(expected.status) ? Number(expected.status) === actual.status : structuralEquals(expected.status, String(actual.status), scope))) return `Response status mismatch (${name})\n expected: ${expected.status}\n received: ${actual.status}`;
643
+ if (!(/^\d+$/u.test(expected.status) ? Number(expected.status) === actual.status : structuralEquals(expected.status, String(actual.status), scope))) return `Response status mismatch (${name})\n expected: ${expected.status}\n received: ${actual.status}`;
643
644
  for (const [key, value] of Object.entries(expected.headers)) {
644
645
  const actualValue = actual.headers[key.toLowerCase()];
645
646
  if (actualValue === void 0 || !structuralEquals(value, actualValue, scope)) return `Response header mismatch (${name})\n header: ${key}\n expected: ${value}\n received: ${actualValue ?? "(absent)"}`;
@@ -756,7 +757,10 @@ function toContain(received, expected) {
756
757
  async function toMatchRows(received, expected) {
757
758
  if (!(received instanceof TableAccessor)) throw new TypeError("toMatchRows: unsupported subject — expected result.table(...).");
758
759
  const actual = await received.query(expected.columns);
759
- const pass = actual.length === expected.rows.length && expected.rows.every((row, i) => row.length === actual[i].length && row.every((cell, j) => structuralEquals(cell, actual[i][j], received.captures)));
760
+ const pass = actual.length === expected.rows.length && expected.rows.every((row, i) => {
761
+ const actualRow = actual[i];
762
+ return actualRow !== void 0 && row.length === actualRow.length && row.every((cell, j) => structuralEquals(cell, actualRow[j], received.captures));
763
+ });
760
764
  return {
761
765
  message: () => pass ? `expected table "${received.name}" not to match the given rows` : formatTableDiff(received.name, expected.columns, expected.rows.map((row) => row.map((cell) => renderExpected(cell))), actual),
762
766
  pass
@@ -815,7 +819,7 @@ async function registerMatchers() {
815
819
  }
816
820
  }
817
821
  //#endregion
818
- //#region src/core/specification/api/result.ts
822
+ //#region src/specification/facets/api/result.ts
819
823
  /** Result from an HTTP action (.request(), .get(), .post(), .put(), .delete()). */
820
824
  var HttpResult = class extends BaseResult {
821
825
  responseData;
@@ -833,7 +837,7 @@ var HttpResult = class extends BaseResult {
833
837
  }
834
838
  };
835
839
  //#endregion
836
- //#region src/core/specification/mobile/result.ts
840
+ //#region src/specification/facets/mobile/result.ts
837
841
  /** Result from a `.open()` action — the screen as the device saw it, final state. */
838
842
  var ScreenResult = class extends BaseResult {
839
843
  capturedScreen;
@@ -857,7 +861,7 @@ var ScreenResult = class extends BaseResult {
857
861
  }
858
862
  };
859
863
  //#endregion
860
- //#region src/core/specification/website/result.ts
864
+ //#region src/specification/facets/website/result.ts
861
865
  /** Result from a raw `.fetch()` action (robots.txt, sitemaps, redirects). */
862
866
  var FetchResult = class extends BaseResult {
863
867
  exchange;
@@ -879,7 +883,7 @@ var FetchResult = class extends BaseResult {
879
883
  }
880
884
  /** The `location` header of a redirect response, or undefined. */
881
885
  get location() {
882
- return this.exchange.headers["location"];
886
+ return this.exchange.headers.location;
883
887
  }
884
888
  /** The HTTP response status code — redirects surface as 3xx, never followed. */
885
889
  get status() {
@@ -979,7 +983,7 @@ function namedMetas(metas) {
979
983
  return named;
980
984
  }
981
985
  //#endregion
982
- //#region src/core/specification/shared/binding.ts
986
+ //#region src/specification/facets/_common/binding.ts
983
987
  /**
984
988
  * Compose-binding resolution and the case conversions it shares with the
985
989
  * automatic env injection (CONVENTIONS A6 / B6).
@@ -995,7 +999,7 @@ function namedMetas(metas) {
995
999
  * `-`, everything is lowercased. `analyticsDb` → `analytics-db`, `db` → `db`.
996
1000
  */
997
1001
  function toKebabCase(key) {
998
- return key.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>-$<upper>").toLowerCase();
1002
+ return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
999
1003
  }
1000
1004
  /**
1001
1005
  * Convert a record key to CONSTANT_CASE for env injection: insert `_` at
@@ -1003,7 +1007,7 @@ function toKebabCase(key) {
1003
1007
  * uppercase. `analyticsDb` → `ANALYTICS_DB`, `db-main` → `DB_MAIN`, `db` → `DB`.
1004
1008
  */
1005
1009
  function toConstantCase(key) {
1006
- return key.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>_$<upper>").replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
1010
+ return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>_$<upper>").replaceAll(/[^A-Za-z0-9]/gu, "_").toUpperCase();
1007
1011
  }
1008
1012
  /**
1009
1013
  * Resolve the compose service a record key binds to (CONVENTIONS A6).
@@ -1028,7 +1032,7 @@ function resolveComposeBinding(key, explicitComposeName, serviceNames) {
1028
1032
  return key;
1029
1033
  }
1030
1034
  //#endregion
1031
- //#region src/core/specification/shared/builder.ts
1035
+ //#region src/specification/facets/_common/builder.ts
1032
1036
  /**
1033
1037
  * Fluent builder for declaring a single test specification.
1034
1038
  *
@@ -1146,8 +1150,8 @@ var SpecificationBuilder = class {
1146
1150
  * @example
1147
1151
  * const result = await api.request("create-user.http");
1148
1152
  */
1149
- request(file) {
1150
- return this.executeHttp({
1153
+ async request(file) {
1154
+ return await this.executeHttp({
1151
1155
  method: "",
1152
1156
  path: "",
1153
1157
  requestFile: file
@@ -1159,8 +1163,8 @@ var SpecificationBuilder = class {
1159
1163
  * @example
1160
1164
  * const result = await api.get("/api/items");
1161
1165
  */
1162
- get(path) {
1163
- return this.executeHttp({
1166
+ async get(path) {
1167
+ return await this.executeHttp({
1164
1168
  method: "GET",
1165
1169
  path
1166
1170
  });
@@ -1173,24 +1177,24 @@ var SpecificationBuilder = class {
1173
1177
  * @example
1174
1178
  * const result = await api.post("/api/items", { name: "Widget" });
1175
1179
  */
1176
- post(path, body) {
1177
- return this.executeHttp({
1180
+ async post(path, body) {
1181
+ return await this.executeHttp({
1178
1182
  body,
1179
1183
  method: "POST",
1180
1184
  path
1181
1185
  });
1182
1186
  }
1183
1187
  /** Send a PUT request to the server adapter and resolve with the result. */
1184
- put(path, body) {
1185
- return this.executeHttp({
1188
+ async put(path, body) {
1189
+ return await this.executeHttp({
1186
1190
  body,
1187
1191
  method: "PUT",
1188
1192
  path
1189
1193
  });
1190
1194
  }
1191
1195
  /** Send a DELETE request to the server adapter and resolve with the result. */
1192
- delete(path) {
1193
- return this.executeHttp({
1196
+ async delete(path) {
1197
+ return await this.executeHttp({
1194
1198
  method: "DELETE",
1195
1199
  path
1196
1200
  });
@@ -1215,10 +1219,10 @@ var SpecificationBuilder = class {
1215
1219
  * const result = await cli.exec(["init", "build"]);
1216
1220
  * const result = await cli.exec("dev --port 0", { waitFor: "Listening on", timeout: 10_000 });
1217
1221
  */
1218
- exec(args = "", options) {
1222
+ async exec(args = "", options) {
1219
1223
  if (Array.isArray(args) && args.length === 0) throw new Error("exec([]) requires at least one command");
1220
1224
  if (options && Array.isArray(args)) throw new Error(".exec(): waitFor/timeout options are not supported with a command sequence");
1221
- return this.executeCommand({
1225
+ return await this.executeCommand({
1222
1226
  args,
1223
1227
  options
1224
1228
  });
@@ -1238,9 +1242,9 @@ var SpecificationBuilder = class {
1238
1242
  * const result = await cli.run('no-estate.spec.yaml');
1239
1243
  * await expect(result.directory('out')).toMatch('scaffold');
1240
1244
  */
1241
- run(file, options) {
1245
+ async run(file, options) {
1242
1246
  const workDir = this.prepareWorkDir();
1243
- return this.executeSetup(workDir, () => this.runLiterateAction(workDir, file, options));
1247
+ return await this.executeSetup(workDir, async () => await this.runLiterateAction(workDir, file, options));
1244
1248
  }
1245
1249
  /**
1246
1250
  * Perform one raw HTTP GET against the website under test and resolve
@@ -1253,8 +1257,8 @@ var SpecificationBuilder = class {
1253
1257
  * expect(result.status).toBe(200);
1254
1258
  * expect(result.body).toMatch('robots.txt');
1255
1259
  */
1256
- fetch(path) {
1257
- return this.executeSetup(null, () => this.runFetchAction(path));
1260
+ async fetch(path) {
1261
+ return await this.executeSetup(null, async () => await this.runFetchAction(path));
1258
1262
  }
1259
1263
  /**
1260
1264
  * Render the page in the shared browser instance and resolve with the
@@ -1271,8 +1275,8 @@ var SpecificationBuilder = class {
1271
1275
  * });
1272
1276
  * expect(result.url).toContain('/articles');
1273
1277
  */
1274
- visit(path, scenario) {
1275
- return this.executeSetup(null, () => this.runVisitAction(path, scenario));
1278
+ async visit(path, scenario) {
1279
+ return await this.executeSetup(null, async () => await this.runVisitAction(path, scenario));
1276
1280
  }
1277
1281
  /**
1278
1282
  * Terminate and relaunch the app on the simulator, apply the deep link,
@@ -1289,8 +1293,8 @@ var SpecificationBuilder = class {
1289
1293
  * await visitor.see(content('rapports'));
1290
1294
  * });
1291
1295
  */
1292
- open(deepLink, scenario) {
1293
- return this.executeSetup(null, () => this.runOpenAction(deepLink, scenario));
1296
+ async open(deepLink, scenario) {
1297
+ return await this.executeSetup(null, async () => await this.runOpenAction(deepLink, scenario));
1294
1298
  }
1295
1299
  /**
1296
1300
  * Execute the named job registered via the `jobs` option of
@@ -1300,14 +1304,14 @@ var SpecificationBuilder = class {
1300
1304
  * const result = await jobs.intercept(classifyArticle).trigger('report-refresh');
1301
1305
  */
1302
1306
  async trigger(name) {
1303
- return this.executeSetup(null, () => this.runJobAction(name));
1307
+ return await this.executeSetup(null, async () => await this.runJobAction(name));
1304
1308
  }
1305
1309
  async executeHttp(request) {
1306
- return this.executeSetup(null, () => this.runHttpAction(request));
1310
+ return await this.executeSetup(null, async () => await this.runHttpAction(request));
1307
1311
  }
1308
1312
  async executeCommand(action) {
1309
1313
  const workDir = this.prepareWorkDir();
1310
- return this.executeSetup(workDir, () => this.runCommandAction(workDir, action));
1314
+ return await this.executeSetup(workDir, async () => await this.runCommandAction(workDir, action));
1311
1315
  }
1312
1316
  /**
1313
1317
  * Shared setup pipeline: reset databases, run seeds, copy fixtures,
@@ -1378,9 +1382,8 @@ var SpecificationBuilder = class {
1378
1382
  }
1379
1383
  async runFetchAction(path) {
1380
1384
  const baseUrl = this.requireBaseUrl("fetch");
1381
- const headers = Object.keys(this.requestHeaders).length > 0 ? this.requestHeaders : void 0;
1382
1385
  const response = await fetch(`${baseUrl}${path}`, {
1383
- headers,
1386
+ headers: this.requestHeaders,
1384
1387
  redirect: "manual"
1385
1388
  });
1386
1389
  const body = await response.text();
@@ -1453,7 +1456,7 @@ var SpecificationBuilder = class {
1453
1456
  * redis). `.env()` overrides; `null` unsets.
1454
1457
  */
1455
1458
  serviceEnv() {
1456
- const services = this.config.services;
1459
+ const { services } = this.config;
1457
1460
  if (!services || Object.keys(services).length === 0) return;
1458
1461
  const env = {};
1459
1462
  const sqlHandles = [];
@@ -1463,8 +1466,10 @@ var SpecificationBuilder = class {
1463
1466
  if (handle.createDatabaseAdapter() !== null) sqlHandles.push(handle);
1464
1467
  if (handle.type === "redis") redisHandles.push(handle);
1465
1468
  }
1466
- if (sqlHandles.length === 1) env.DATABASE_URL = sqlHandles[0].connectionString;
1467
- if (redisHandles.length === 1) env.REDIS_URL = redisHandles[0].connectionString;
1469
+ const [onlySql] = sqlHandles;
1470
+ if (sqlHandles.length === 1 && onlySql !== void 0) env.DATABASE_URL = onlySql.connectionString;
1471
+ const [onlyRedis] = redisHandles;
1472
+ if (redisHandles.length === 1 && onlyRedis !== void 0) env.REDIS_URL = onlyRedis.connectionString;
1468
1473
  return env;
1469
1474
  }
1470
1475
  /**
@@ -1474,7 +1479,7 @@ var SpecificationBuilder = class {
1474
1479
  */
1475
1480
  childEnv(workDir) {
1476
1481
  let env = this.serviceEnv();
1477
- const dockerConfig = this.config.dockerConfig;
1482
+ const { dockerConfig } = this.config;
1478
1483
  if (dockerConfig && this.config.dockerTestRunId) env = {
1479
1484
  ...env,
1480
1485
  [dockerConfig.envVar]: this.config.dockerTestRunId
@@ -1486,9 +1491,9 @@ var SpecificationBuilder = class {
1486
1491
  };
1487
1492
  return env;
1488
1493
  }
1489
- runLiterateAction(workDir, file, options) {
1494
+ async runLiterateAction(workDir, file, options) {
1490
1495
  const filePath = resolve(this.testDir, file);
1491
- return runSpecDocument({
1496
+ return await runSpecDocument({
1492
1497
  baseEnv: this.childEnv(workDir),
1493
1498
  frozen: options?.frozen,
1494
1499
  config: this.config,
@@ -1500,7 +1505,7 @@ var SpecificationBuilder = class {
1500
1505
  }
1501
1506
  async runCommandAction(workDir, action) {
1502
1507
  if (!this.config.command) throw new Error("Command actions require a command adapter");
1503
- const dockerConfig = this.config.dockerConfig;
1508
+ const { dockerConfig } = this.config;
1504
1509
  const testRunId = this.config.dockerTestRunId;
1505
1510
  const env = this.childEnv(workDir);
1506
1511
  let commandOutput;
@@ -1549,13 +1554,13 @@ function withDockerTestRunId(config) {
1549
1554
  function createApiFacet(config) {
1550
1555
  const start = () => new SpecificationBuilder(config, getCallerDir());
1551
1556
  return {
1552
- delete: (path) => start().delete(path),
1553
- get: (path) => start().get(path),
1557
+ delete: async (path) => await start().delete(path),
1558
+ get: async (path) => await start().get(path),
1554
1559
  headers: (headers) => start().headers(headers),
1555
1560
  intercept: interceptOn(start),
1556
- post: (path, body) => start().post(path, body),
1557
- put: (path, body) => start().put(path, body),
1558
- request: (file) => start().request(file),
1561
+ post: async (path, body) => await start().post(path, body),
1562
+ put: async (path, body) => await start().put(path, body),
1563
+ request: async (file) => await start().request(file),
1559
1564
  seed: (file, options) => start().seed(file, options)
1560
1565
  };
1561
1566
  }
@@ -1567,7 +1572,7 @@ function createJobsFacet(config) {
1567
1572
  return {
1568
1573
  intercept: interceptOn(start),
1569
1574
  seed: (file, options) => start().seed(file, options),
1570
- trigger: (name) => start().trigger(name)
1575
+ trigger: async (name) => await start().trigger(name)
1571
1576
  };
1572
1577
  }
1573
1578
  /**
@@ -1576,10 +1581,10 @@ function createJobsFacet(config) {
1576
1581
  function createWebsiteFacet(config) {
1577
1582
  const start = () => new SpecificationBuilder(config, getCallerDir());
1578
1583
  return {
1579
- fetch: (path) => start().fetch(path),
1584
+ fetch: async (path) => await start().fetch(path),
1580
1585
  headers: (headers) => start().headers(headers),
1581
1586
  intercept: interceptOn(start),
1582
- visit: (path, scenario) => start().visit(path, scenario)
1587
+ visit: async (path, scenario) => await start().visit(path, scenario)
1583
1588
  };
1584
1589
  }
1585
1590
  /**
@@ -1589,7 +1594,7 @@ function createMobileFacet(config) {
1589
1594
  const start = () => new SpecificationBuilder(config, getCallerDir());
1590
1595
  return {
1591
1596
  intercept: interceptOn(start),
1592
- open: (deepLink, scenario) => start().open(deepLink, scenario)
1597
+ open: async (deepLink, scenario) => await start().open(deepLink, scenario)
1593
1598
  };
1594
1599
  }
1595
1600
  /**
@@ -1600,14 +1605,14 @@ function createCliFacet(config) {
1600
1605
  const start = () => new SpecificationBuilder(resolved, getCallerDir());
1601
1606
  return {
1602
1607
  env: (env) => start().env(env),
1603
- exec: (args, options) => start().exec(args, options),
1608
+ exec: async (args, options) => await start().exec(args, options),
1604
1609
  fixture: (path) => start().fixture(path),
1605
- run: (file, options) => start().run(file, options),
1610
+ run: async (file, options) => await start().run(file, options),
1606
1611
  seed: (file, options) => start().seed(file, options)
1607
1612
  };
1608
1613
  }
1609
1614
  //#endregion
1610
- //#region src/core/specification/shared/docker-reader.ts
1615
+ //#region src/specification/facets/_common/docker-reader.ts
1611
1616
  /**
1612
1617
  * Build the `docker(containerId)` reader handed out by `specification.api()`
1613
1618
  * and `specification.cli()` — a lazy accessor over `docker inspect`.
@@ -1622,7 +1627,7 @@ function createDockerReader(testDir) {
1622
1627
  };
1623
1628
  }
1624
1629
  //#endregion
1625
- //#region src/core/specification/shared/compose-file.ts
1630
+ //#region src/specification/facets/_common/compose-file.ts
1626
1631
  /**
1627
1632
  * Detect the service type from the image name.
1628
1633
  */
@@ -1648,7 +1653,7 @@ function findComposeFile(projectRoot) {
1648
1653
  return null;
1649
1654
  }
1650
1655
  //#endregion
1651
- //#region src/core/specification/shared/orchestrator.ts
1656
+ //#region src/specification/facets/_common/orchestrator.ts
1652
1657
  /**
1653
1658
  * Orchestrator for test infrastructure.
1654
1659
  * Integration: starts services via testcontainers.
@@ -1721,7 +1726,9 @@ var Orchestrator = class {
1721
1726
  handle
1722
1727
  });
1723
1728
  }
1724
- await Promise.all([...containerServices.map(({ container }) => container.start()), ...embeddedServices.map(async (handle) => {
1729
+ await Promise.all([...containerServices.map(async ({ container }) => {
1730
+ await container.start();
1731
+ }), ...embeddedServices.map(async (handle) => {
1725
1732
  await handle.initialize(composeDir, this.root);
1726
1733
  handle.started = true;
1727
1734
  this.running.push({
@@ -1880,13 +1887,14 @@ var Orchestrator = class {
1880
1887
  getAppUrl() {
1881
1888
  const composePath = findComposeFile(this.root);
1882
1889
  if (!composePath || !this.composeStack) return null;
1883
- const appService = getContainerIntegrations().parseComposeFile(composePath).appService;
1884
- if (!appService || appService.ports.length === 0) return null;
1885
- return `http://localhost:${this.composeStack.getMappedPort(appService.name, appService.ports[0].container)}`;
1890
+ const { appService } = getContainerIntegrations().parseComposeFile(composePath);
1891
+ const [firstPort] = appService?.ports ?? [];
1892
+ if (!appService || firstPort === void 0) return null;
1893
+ return `http://localhost:${this.composeStack.getMappedPort(appService.name, firstPort.container)}`;
1886
1894
  }
1887
1895
  };
1888
1896
  //#endregion
1889
- //#region src/core/specification/shared/services.ts
1897
+ //#region src/specification/facets/_common/services.ts
1890
1898
  function getWorkerId() {
1891
1899
  return process.env.VITEST_POOL_ID ?? "0";
1892
1900
  }
@@ -1898,7 +1906,7 @@ async function releaseIsolation(services) {
1898
1906
  for (const service of Object.values(services)) await service.isolation().release();
1899
1907
  }
1900
1908
  function declaredDatabaseKeys(services) {
1901
- return Object.keys(services).filter((key) => services[key].createDatabaseAdapter() !== null);
1909
+ return Object.entries(services).filter(([, service]) => service.createDatabaseAdapter() !== null).map(([key]) => key);
1902
1910
  }
1903
1911
  /** Start a services record via testcontainers and acquire worker isolation. */
1904
1912
  async function startServices(services, root) {
@@ -1917,7 +1925,7 @@ async function startServices(services, root) {
1917
1925
  };
1918
1926
  }
1919
1927
  //#endregion
1920
- //#region src/core/specification/api/fetch.adapter.ts
1928
+ //#region src/specification/facets/api/fetch.adapter.ts
1921
1929
  /**
1922
1930
  * Transient connection-level failures against a live compose app. Under
1923
1931
  * parallel load (every `api-stack` worker hammering its own compose project)
@@ -1940,7 +1948,9 @@ function isTransientConnectionError(error) {
1940
1948
  const cause = error.cause;
1941
1949
  return cause?.code !== void 0 && TRANSIENT_CONNECTION_CODES.has(cause.code) || error.message.includes("fetch failed");
1942
1950
  }
1943
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1951
+ const delay = async (ms) => {
1952
+ await new Promise((resolve) => setTimeout(resolve, ms));
1953
+ };
1944
1954
  /**
1945
1955
  * Server adapter that sends real HTTP requests via the Fetch API.
1946
1956
  * Used by the `e2e()` specification runner to hit a live server.
@@ -1948,7 +1958,7 @@ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1948
1958
  var FetchAdapter = class {
1949
1959
  baseUrl;
1950
1960
  constructor(url) {
1951
- this.baseUrl = url.replace(/\/$/, "");
1961
+ this.baseUrl = url.replace(/\/$/u, "");
1952
1962
  }
1953
1963
  async request(method, path, body, headers) {
1954
1964
  const init = {
@@ -1986,7 +1996,7 @@ var FetchAdapter = class {
1986
1996
  }
1987
1997
  };
1988
1998
  //#endregion
1989
- //#region src/core/specification/api/start-api.ts
1999
+ //#region src/specification/facets/api/start-api.ts
1990
2000
  function resolveMode(explicit) {
1991
2001
  const value = explicit ?? process.env.TEST_MODE ?? "node";
1992
2002
  if (value !== "compose" && value !== "node") throw new Error(`Invalid test mode "${value}" — expected 'node' or 'compose' (options.mode or TEST_MODE).`);
@@ -2035,13 +2045,15 @@ async function startApi(options) {
2035
2045
  interceptDisabledReason: "intercepts are in-process (MSW) and not available in compose mode — keep intercept specs in node-only vitest projects.",
2036
2046
  server: new FetchAdapter(appUrl)
2037
2047
  }),
2038
- cleanup: () => orchestrator.stopCompose(),
2048
+ cleanup: async () => {
2049
+ await orchestrator.stopCompose();
2050
+ },
2039
2051
  docker: createDockerReader(callerDir),
2040
2052
  orchestrator
2041
2053
  };
2042
2054
  }
2043
2055
  //#endregion
2044
- //#region src/core/specification/cli/exec.adapter.ts
2056
+ //#region src/specification/facets/cli/exec.adapter.ts
2045
2057
  const DEFAULT_WATCH_TIMEOUT = 1e4;
2046
2058
  /** Grace period between SIGTERM and the SIGKILL escalation. */
2047
2059
  const KILL_GRACE_MS = 2e3;
@@ -2070,17 +2082,19 @@ function buildEnv(extra) {
2070
2082
  *
2071
2083
  * @internal Exported for unit tests (driven with a fake child).
2072
2084
  */
2073
- function observeProcess(child, options) {
2085
+ async function observeProcess(child, options) {
2074
2086
  const timeout = options.timeout ?? DEFAULT_WATCH_TIMEOUT;
2075
- const waitFor = options.waitFor;
2076
- return new Promise((resolve) => {
2087
+ const { waitFor } = options;
2088
+ return await new Promise((resolve) => {
2077
2089
  let stdout = "";
2078
2090
  let stderr = "";
2079
2091
  let resolved = false;
2080
2092
  let exited = false;
2081
2093
  let patternMatched = false;
2082
2094
  let killTimer = null;
2083
- const timeoutTimer = setTimeout(() => finish(124), timeout);
2095
+ const timeoutTimer = setTimeout(() => {
2096
+ finish(124);
2097
+ }, timeout);
2084
2098
  timeoutTimer.unref?.();
2085
2099
  const terminate = () => {
2086
2100
  if (exited) return;
@@ -2185,7 +2199,7 @@ var ExecAdapter = class {
2185
2199
  } catch {}
2186
2200
  return child.kill(signal);
2187
2201
  };
2188
- return observeProcess({
2202
+ return await observeProcess({
2189
2203
  kill,
2190
2204
  on: child.on.bind(child),
2191
2205
  stderr: child.stderr,
@@ -2194,7 +2208,7 @@ var ExecAdapter = class {
2194
2208
  }
2195
2209
  };
2196
2210
  //#endregion
2197
- //#region src/core/specification/cli/start-cli.ts
2211
+ //#region src/specification/facets/cli/start-cli.ts
2198
2212
  async function startCli(bin, options = {}) {
2199
2213
  const callerDir = getCallerDir();
2200
2214
  await registerMatchers();
@@ -2226,7 +2240,7 @@ async function startCli(bin, options = {}) {
2226
2240
  };
2227
2241
  }
2228
2242
  //#endregion
2229
- //#region src/core/specification/jobs/start-jobs.ts
2243
+ //#region src/specification/facets/jobs/start-jobs.ts
2230
2244
  async function startJobs(options) {
2231
2245
  const callerDir = getCallerDir();
2232
2246
  await registerMatchers();
@@ -2251,7 +2265,7 @@ async function startJobs(options) {
2251
2265
  };
2252
2266
  }
2253
2267
  //#endregion
2254
- //#region src/core/specification/shared/stub-backend.ts
2268
+ //#region src/specification/facets/_common/stub-backend.ts
2255
2269
  /**
2256
2270
  * The declared stub backend — a small `node:http` server the website and
2257
2271
  * mobile runners start when their `backend` option is present. It serves the
@@ -2295,7 +2309,9 @@ var StubBackend = class {
2295
2309
  this.server = server;
2296
2310
  await new Promise((resolve, reject) => {
2297
2311
  server.once("error", reject);
2298
- server.listen(this.options.port ?? 0, "127.0.0.1", () => resolve());
2312
+ server.listen(this.options.port ?? 0, "127.0.0.1", () => {
2313
+ resolve();
2314
+ });
2299
2315
  });
2300
2316
  const address = server.address();
2301
2317
  if (address === null || typeof address === "string") throw new Error("stub backend: could not determine the listening port");
@@ -2304,12 +2320,14 @@ var StubBackend = class {
2304
2320
  }
2305
2321
  /** Stop the server (idempotent). */
2306
2322
  async stop() {
2307
- const server = this.server;
2323
+ const { server } = this;
2308
2324
  if (!server) return;
2309
2325
  this.server = null;
2310
2326
  server.closeAllConnections();
2311
2327
  await new Promise((resolve) => {
2312
- server.close(() => resolve());
2328
+ server.close(() => {
2329
+ resolve();
2330
+ });
2313
2331
  });
2314
2332
  }
2315
2333
  /**
@@ -2423,7 +2441,7 @@ async function readBody(request) {
2423
2441
  }
2424
2442
  }
2425
2443
  //#endregion
2426
- //#region src/core/specification/mobile/appium-server.ts
2444
+ //#region src/specification/facets/mobile/appium-server.ts
2427
2445
  const APPIUM_READY_TIMEOUT_MS = 6e4;
2428
2446
  /**
2429
2447
  * The guidance thrown when the optional peers are missing — the exact fix,
@@ -2447,12 +2465,14 @@ async function startAppiumServer(root) {
2447
2465
  timeout: APPIUM_READY_TIMEOUT_MS
2448
2466
  }, root, "mobile");
2449
2467
  return {
2450
- stop: () => serve.stop(),
2468
+ stop: async () => {
2469
+ await serve.stop();
2470
+ },
2451
2471
  url: await serve.start()
2452
2472
  };
2453
2473
  }
2454
2474
  //#endregion
2455
- //#region src/core/specification/mobile/simulator.ts
2475
+ //#region src/specification/facets/mobile/simulator.ts
2456
2476
  /**
2457
2477
  * Simulator resolution — `device: { name, os? }` → one booted UDID.
2458
2478
  *
@@ -2465,8 +2485,10 @@ const run = promisify(execFile);
2465
2485
  const BOOT_TIMEOUT_MS = 12e4;
2466
2486
  /** `com.apple.CoreSimulator.SimRuntime.iOS-26-5` → `iOS 26.5`. */
2467
2487
  function runtimeLabel(runtime) {
2468
- const match = /SimRuntime\.(?<platform>[A-Za-z]+)-(?<version>[\d-]+)$/.exec(runtime);
2469
- return match?.groups ? `${match.groups["platform"]} ${match.groups["version"].replaceAll("-", ".")}` : runtime;
2488
+ const match = /SimRuntime\.(?<platform>[A-Za-z]+)-(?<version>[\d-]+)$/u.exec(runtime);
2489
+ const platform = match?.groups?.platform;
2490
+ const version = match?.groups?.version;
2491
+ return platform === void 0 || version === void 0 ? runtime : `${platform} ${version.replaceAll("-", ".")}`;
2470
2492
  }
2471
2493
  /** Flatten the per-runtime record into one available-device list. */
2472
2494
  function listSimulators(listJson) {
@@ -2495,9 +2517,10 @@ function selectSimulator(listJson, criteria) {
2495
2517
  const available = listSimulators(listJson);
2496
2518
  const matches = available.filter((device) => device.name === criteria.name && (criteria.os === void 0 || osMatches(device.os, criteria.os)));
2497
2519
  const wanted = criteria.os === void 0 ? `"${criteria.name}"` : `"${criteria.name}" on ${criteria.os}`;
2498
- if (matches.length === 0) throw new Error(`specification.mobile(): no simulator matches ${wanted}.\nAvailable devices:\n${formatListing(available)}`);
2520
+ const [only] = matches;
2521
+ if (only === void 0) throw new Error(`specification.mobile(): no simulator matches ${wanted}.\nAvailable devices:\n${formatListing(available)}`);
2499
2522
  if (matches.length > 1) throw new Error(`specification.mobile(): ${matches.length} simulators match ${wanted} — add \`os:\` or \`udid:\` to pick one.\nMatched:\n${formatListing(matches)}`);
2500
- return matches[0];
2523
+ return only;
2501
2524
  }
2502
2525
  /** Resolve `device: { name, os? }` to a UDID via `xcrun simctl`. */
2503
2526
  async function resolveSimulatorUdid(criteria) {
@@ -2533,7 +2556,7 @@ async function ensureBooted(udid) {
2533
2556
  ], { timeout: BOOT_TIMEOUT_MS });
2534
2557
  }
2535
2558
  //#endregion
2536
- //#region src/core/specification/mobile/start-mobile.ts
2559
+ //#region src/specification/facets/mobile/start-mobile.ts
2537
2560
  async function startMobile(options) {
2538
2561
  const callerDir = getCallerDir();
2539
2562
  await registerMatchers();
@@ -2580,7 +2603,7 @@ async function startMobile(options) {
2580
2603
  };
2581
2604
  }
2582
2605
  //#endregion
2583
- //#region src/core/specification/website/start-website.ts
2606
+ //#region src/specification/facets/website/start-website.ts
2584
2607
  async function startWebsite(options) {
2585
2608
  const callerDir = getCallerDir();
2586
2609
  await registerMatchers();
@@ -2602,7 +2625,7 @@ async function startWebsite(options) {
2602
2625
  await backend?.stop();
2603
2626
  throw error;
2604
2627
  }
2605
- } else baseUrl = options.url.replace(/\/$/, "");
2628
+ } else baseUrl = options.url.replace(/\/$/u, "");
2606
2629
  let browser = null;
2607
2630
  const getBrowser = async () => {
2608
2631
  if (!browser) {
@@ -2632,31 +2655,7 @@ async function startWebsite(options) {
2632
2655
  };
2633
2656
  }
2634
2657
  //#endregion
2635
- //#region src/core/specification/shared/specification.ts
2636
- /**
2637
- * The five specification constructors (CONVENTIONS A2) — created in a
2638
- * `*.specification.ts` file under `specs/`, destructured with canonical
2639
- * names, and cleaned up via `afterAll(cleanup)` (A1/A3/A4).
2640
- *
2641
- * @example
2642
- * // specs/api/api.specification.ts
2643
- * export const { api, cleanup } = await specification.api({
2644
- * services: { db: postgres() },
2645
- * server: ({ db }) => createApp({ databaseUrl: db.connectionString }),
2646
- * });
2647
- * afterAll(cleanup);
2648
- *
2649
- * // specs/jobs/jobs.specification.ts
2650
- * export const { jobs, cleanup } = await specification.jobs({
2651
- * services: { db: postgres() },
2652
- * jobs: ({ db }) => [nightlyReport(db)],
2653
- * });
2654
- * afterAll(cleanup);
2655
- *
2656
- * // specs/setup/cli.specification.ts
2657
- * export const { cli, cleanup } = await specification.cli('my-cli');
2658
- * afterAll(cleanup);
2659
- */
2658
+ //#region src/specification/facets/_common/specification.ts
2660
2659
  const specification = {
2661
2660
  /**
2662
2661
  * Test an HTTP app. Mode `'node'` (default) starts the declared services
@@ -2694,7 +2693,7 @@ const specification = {
2694
2693
  website: startWebsite
2695
2694
  };
2696
2695
  //#endregion
2697
- //#region src/core/specification/website/elements.ts
2696
+ //#region src/specification/facets/website/elements.ts
2698
2697
  const named = (kind) => (name, options) => ({
2699
2698
  kind,
2700
2699
  name,
@@ -3078,7 +3077,7 @@ function sqlite(options = {}) {
3078
3077
  return new SqliteHandle(options);
3079
3078
  }
3080
3079
  //#endregion
3081
- //#region src/core/contracts/filters.ts
3080
+ //#region src/specification/contracts/filters.ts
3082
3081
  /** Does an observed text satisfy a declared {@link TextFilter}? */
3083
3082
  function matchesText(filter, actual) {
3084
3083
  if (filter === void 0) return true;
@@ -3152,7 +3151,9 @@ const anthropic = {
3152
3151
  };
3153
3152
  },
3154
3153
  /** Response: wrap data in Anthropic messages format. */
3155
- reply: buildReply,
3154
+ reply(data) {
3155
+ return buildReply(data);
3156
+ },
3156
3157
  /** Response: return an Anthropic error. */
3157
3158
  error(status, message) {
3158
3159
  return {
@@ -3176,7 +3177,7 @@ const anthropic = {
3176
3177
  }
3177
3178
  };
3178
3179
  //#endregion
3179
- //#region src/core/contracts/http.ts
3180
+ //#region src/specification/contracts/http.ts
3180
3181
  function wrapJson(data) {
3181
3182
  return {
3182
3183
  status: 200,
@@ -3426,7 +3427,9 @@ const openai = {
3426
3427
  * @example
3427
3428
  * openai.reply({ categories: ['TECH'] })
3428
3429
  */
3429
- reply: buildChatReply,
3430
+ reply(data) {
3431
+ return buildChatReply(data);
3432
+ },
3430
3433
  /** Response: return an OpenAI error. */
3431
3434
  error(status, message) {
3432
3435
  return {