@intentius/chant 0.66.0 → 0.67.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 (36) hide show
  1. package/dist/behaviour-engine.d.ts +212 -0
  2. package/dist/behaviour-engine.d.ts.map +1 -0
  3. package/dist/behaviour-http.d.ts +1 -1
  4. package/dist/behaviour-http.d.ts.map +1 -1
  5. package/dist/behaviour-kinds.d.ts +220 -0
  6. package/dist/behaviour-kinds.d.ts.map +1 -0
  7. package/dist/behaviour-predict.d.ts +83 -0
  8. package/dist/behaviour-predict.d.ts.map +1 -0
  9. package/dist/behaviour-request.d.ts +141 -0
  10. package/dist/behaviour-request.d.ts.map +1 -0
  11. package/dist/behaviour.d.ts +7 -7
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/lexicon.d.ts +19 -0
  15. package/dist/lexicon.d.ts.map +1 -1
  16. package/dist/op/activities/index.d.ts +1 -1
  17. package/dist/op/activities/index.d.ts.map +1 -1
  18. package/dist/op/activities/predict-behaviour.d.ts +12 -5
  19. package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/behaviour-delta.test.ts +8 -8
  22. package/src/behaviour-engine.ts +478 -0
  23. package/src/behaviour-http.ts +1 -1
  24. package/src/behaviour-kinds.test.ts +235 -0
  25. package/src/behaviour-kinds.ts +324 -0
  26. package/src/behaviour-predict.ts +253 -0
  27. package/src/behaviour-request.ts +294 -0
  28. package/src/behaviour.ts +7 -7
  29. package/src/cli/handlers/scenario.test.ts +1 -1
  30. package/src/cli/handlers/scenario.ts +1 -1
  31. package/src/index.ts +1 -0
  32. package/src/lexicon.ts +20 -0
  33. package/src/lifecycle/scenario-cost.test.ts +2 -2
  34. package/src/op/activities/index.ts +1 -2
  35. package/src/op/activities/predict-behaviour.test.ts +20 -7
  36. package/src/op/activities/predict-behaviour.ts +32 -30
@@ -0,0 +1,478 @@
1
+ /**
2
+ * The seam between core and whatever answers its request (#2357),
3
+ * on the contract's transport (#2373, decided in #2359).
4
+ *
5
+ * `packages/core/src/behaviour.ts` says what a prediction may mean, how an
6
+ * absent engine must refuse, and — since #2359 — how a request reaches an
7
+ * engine: a `BehaviourTransport` carries the rendered request and brings back
8
+ * the engine's text or a finished refusal. This file adds what is core's on
9
+ * top of that and nothing more: the parse of a `behaviour/v1` answer, a command
10
+ * transport for an address that is a program on `PATH`, and the chooser that
11
+ * turns an address into one transport or the other.
12
+ *
13
+ * {@link BehaviourEngine} is what `./predict-behaviour.ts` talks to. It is
14
+ * one level above the transport — request in, parsed answer or refusal out —
15
+ * so the fixture engine in `__fixtures__` can be one without pretending to be
16
+ * a wire, and so `predict-behaviour.ts` never sees a byte. {@link
17
+ * transportEngine} is the only bridge between the two levels.
18
+ *
19
+ * ## The two transports
20
+ *
21
+ * A **URL** is dialled by core's `httpBehaviourTransport`
22
+ * (`packages/core/src/behaviour-http.ts`): `POST`, a bearer token from
23
+ * `CHANT_BEHAVIOUR_TOKEN` → `BEHAVIOUR_TOKEN`,
24
+ * and the status mapping the contract fixes. Nothing about that is core's,
25
+ * which is why it does not live here.
26
+ *
27
+ * A **command on PATH** is {@link commandTransport}, below: request on stdin,
28
+ * answer on stdout, no shell — a shell would make the address a
29
+ * code-execution surface for whatever set the variable. The child gets
30
+ * `behaviourEngineChildEnvironment()` and nothing else, for the reason given
31
+ * on that function: the request-side screen cannot see an inherited
32
+ * `process.env`. An engine that answered and still refused says so on stderr,
33
+ * and the words it uses pick the cause.
34
+ *
35
+ * ## A malformed answer is unreachable, not a report with holes
36
+ *
37
+ * Taking the fields that parsed and reporting the rest unpredicted would turn
38
+ * an engine emitting garbage into an estate that looks partly free — the
39
+ * failure the refusal arm exists to prevent, one level down. So the parse
40
+ * refuses whole, with a detail naming the entity and the field.
41
+ *
42
+ * With one exception, and it is the same rule the command transport applies
43
+ * to stderr: an engine that took the request, answered `200`, and wrote
44
+ * `{"error": "out of credit"}` has refused for a reason a status never
45
+ * carried. {@link transportEngine} reads that body's words after the parse
46
+ * has failed and never before, so an answer that priced the estate and
47
+ * declined one node "rate limit reached for this region" stays the report it
48
+ * is. Both transports reach the same three causes; only the evidence differs.
49
+ */
50
+
51
+ import { execFile, type ExecFileException } from "node:child_process";
52
+ import {
53
+ behaviourEngineChildEnvironment,
54
+ behaviourWireRefusal,
55
+ isBehaviourBasis,
56
+ isResilienceVerdict,
57
+ unreachableBehaviourEngineRefusal,
58
+ } from "./behaviour";
59
+ import type {
60
+ BehaviourBasis,
61
+ BehaviourEngineEndpoint,
62
+ BehaviourRefusalReport,
63
+ BehaviourTransport,
64
+ BehaviourWireCause,
65
+ ResilienceVerdict,
66
+ } from "./behaviour";
67
+ import { httpBehaviourTransport, isHttpBehaviourAddress } from "./behaviour-http";
68
+ import type { HttpBehaviourTransportDeps } from "./behaviour-http";
69
+ import type { EngineRequest } from "./behaviour-request";
70
+ import { renderEngineRequest } from "./behaviour-request";
71
+
72
+ /**
73
+ * The name a refusal is built under.
74
+ *
75
+ * It was the predicting lexicon's name while a lexicon predicted. Core
76
+ * predicts for the whole project since #2382, so there is no lexicon to scope
77
+ * the variable chain by: the chain is the chant-wide one, and this is the word
78
+ * a message uses for the thing that refused.
79
+ */
80
+ export const BEHAVIOUR_SCOPE = "chant";
81
+
82
+ /** One entity's figures, as the engine states them. */
83
+ export interface EngineFigure {
84
+ perHour: number;
85
+ currency: string;
86
+ headroom: { cpu?: number; latency?: number };
87
+ errorRate: number;
88
+ resilience: { failure: string; verdict: ResilienceVerdict; note?: string };
89
+ rightSize?: { suggestion: string; reason?: string };
90
+ }
91
+
92
+ /** A run the engine answered. */
93
+ export interface EngineAnswer {
94
+ /** How the engine names itself, its version, and the tolerance it states. */
95
+ engine: string;
96
+ version: string;
97
+ tolerance: string;
98
+ basis: BehaviourBasis;
99
+ /** An estate total, when the engine states one of its own. Never chant's sum. */
100
+ total?: { perHour: number; currency: string };
101
+ /** Figures, keyed by the node name the request used. */
102
+ figures: Record<string, EngineFigure>;
103
+ /**
104
+ * Nodes the engine was sent and declined, keyed by name, with its reason.
105
+ * Separate from an absent key: a node in neither map is a defect, and
106
+ * `./predict-behaviour.ts` reports it rather than dropping it.
107
+ */
108
+ declined?: Record<string, string>;
109
+ }
110
+
111
+ /**
112
+ * What an engine call comes back with: a parsed answer, or a refusal built
113
+ * by whoever saw the failure — the transport for a wire condition, this file
114
+ * for an answer that does not parse. `./predict-behaviour.ts` returns the
115
+ * refusal as it stands and never rebuilds one, which is how the contract's
116
+ * four causes stay four remedies rather than one lexicon's guess.
117
+ */
118
+ export type EngineOutcome = { ok: true; answer: EngineAnswer } | { ok: false; refusal: BehaviourRefusalReport };
119
+
120
+ /** Whatever answers a request, one level above the wire. The fixture engine is one of these. */
121
+ export interface BehaviourEngine {
122
+ predict(request: EngineRequest): Promise<EngineOutcome>;
123
+ }
124
+
125
+ /**
126
+ * Resolve an address to an engine, or to `undefined` when nothing here
127
+ * speaks it. `env` is where a transport that authenticates reads its token
128
+ * from, and a chooser that has no use for it may ignore it. A caller that
129
+ * gets `undefined` refuses as `engine-unreachable` naming the address, which
130
+ * is the honest verdict for an address chant cannot dial.
131
+ */
132
+ export type EngineConnect = (
133
+ endpoint: BehaviourEngineEndpoint,
134
+ env: Record<string, string | undefined>,
135
+ ) => BehaviourEngine | undefined;
136
+
137
+ /**
138
+ * A {@link BehaviourEngine} over a contract transport: render the request
139
+ * canonically, send it, parse what came back. The one bridge between the
140
+ * transport level and the engine level, so the parse runs on every wire and
141
+ * no transport gets its own.
142
+ */
143
+ export function transportEngine(
144
+ transport: BehaviourTransport,
145
+ endpoint: BehaviourEngineEndpoint,
146
+ ): BehaviourEngine {
147
+ return {
148
+ async predict(request: EngineRequest): Promise<EngineOutcome> {
149
+ const sent = await transport.send(renderEngineRequest(request));
150
+ if (!sent.ok) return { ok: false, refusal: sent.refusal };
151
+ const parsed = parseEngineAnswer(sent.body);
152
+ if (!parsed.ok) {
153
+ // An engine that answers 200 with an error envelope is a real shape,
154
+ // and its own words are the only thing that says which refusal it is.
155
+ // Read after the parse and never before: an answer that priced the
156
+ // estate and declined one node "rate limit reached for this region"
157
+ // parses, and a vocabulary check running first would turn a report
158
+ // about the other nodes into an over-quota refusal about none.
159
+ const said = causeFromEngineWords(sent.body);
160
+ if (said) {
161
+ return {
162
+ ok: false,
163
+ refusal: behaviourWireRefusal(BEHAVIOUR_SCOPE, endpoint, said, `the engine answered ${firstLine(sent.body)}`),
164
+ };
165
+ }
166
+ return { ok: false, refusal: unreachableBehaviourEngineRefusal(BEHAVIOUR_SCOPE, endpoint, parsed.detail) };
167
+ }
168
+ return { ok: true, answer: parsed.answer };
169
+ },
170
+ };
171
+ }
172
+
173
+ /** How long a command engine is given before it is treated as unreachable. */
174
+ const COMMAND_TIMEOUT_MS = 30_000;
175
+
176
+ /** How much stdout is read before the answer is treated as malformed. */
177
+ const COMMAND_MAX_BUFFER = 8 * 1024 * 1024;
178
+
179
+ /**
180
+ * A `command on PATH` address, run as a subprocess: the contract's
181
+ * `BehaviourTransport` for the third kind of address it names.
182
+ *
183
+ * The address is split on whitespace into a program and its arguments, which
184
+ * is the shape `CHANT_BEHAVIOUR_ENGINE="my-engine --model tiny"` produces.
185
+ * No shell: a shell would make the address a code-execution surface for
186
+ * whatever set the variable, and every argument the address needs can be
187
+ * written without one.
188
+ */
189
+ export function commandTransport(endpoint: BehaviourEngineEndpoint): BehaviourTransport {
190
+ const [program, ...args] = endpoint.value.trim().split(/\s+/);
191
+ return {
192
+ async send(body: string) {
193
+ const raw = await new Promise<{ stdout: string; error?: ExecFileException; stderr: string }>(
194
+ (resolve) => {
195
+ const child = execFile(
196
+ program,
197
+ args,
198
+ {
199
+ timeout: COMMAND_TIMEOUT_MS,
200
+ maxBuffer: COMMAND_MAX_BUFFER,
201
+ // Not `process.env`. The contract's rule, and its reason, are on
202
+ // `behaviourEngineChildEnvironment`; the test below pins it.
203
+ env: behaviourEngineChildEnvironment(),
204
+ },
205
+ (error, stdout, stderr) => {
206
+ resolve({ stdout: String(stdout), stderr: String(stderr), ...(error ? { error } : {}) });
207
+ },
208
+ );
209
+ child.stdin?.end(body);
210
+ },
211
+ );
212
+
213
+ if (raw.error) {
214
+ return {
215
+ ok: false,
216
+ refusal: behaviourWireRefusal(
217
+ BEHAVIOUR_SCOPE,
218
+ endpoint,
219
+ causeFromEngineWords(raw.stderr) ?? "engine-unreachable",
220
+ firstLine(raw.stderr) || raw.error.message,
221
+ ),
222
+ };
223
+ }
224
+ return { ok: true, body: raw.stdout };
225
+ },
226
+ };
227
+ }
228
+
229
+ /** {@link commandTransport}, bridged to the engine level. */
230
+ export function commandEngine(endpoint: BehaviourEngineEndpoint): BehaviourEngine {
231
+ return transportEngine(commandTransport(endpoint), endpoint);
232
+ }
233
+
234
+ /**
235
+ * An engine that answered and still refused says so in words, and the three
236
+ * causes want three different actions (`behaviour.ts`'s remedy table). Matched
237
+ * on the words an engine would use rather than on an exit code or an HTTP
238
+ * status, because the contract fixes no exit codes, inventing some would bind
239
+ * every future engine to this file, and a status is only available on one of
240
+ * the two transports anyway.
241
+ *
242
+ * Both transports read it, on the text each has: a command's stderr, and an
243
+ * HTTP body that failed to parse as an answer. `./behaviour-http.ts` has
244
+ * already classified every status that carries one, so this runs on the case
245
+ * a status does not cover — the engine that took the request, answered 200,
246
+ * and put its refusal in the envelope.
247
+ */
248
+ function causeFromEngineWords(said: string): BehaviourWireCause | undefined {
249
+ const text = said.toLowerCase();
250
+ if (/\b(out of credit|insufficient (funds|balance)|no balance|payment required)\b/.test(text)) {
251
+ return "engine-out-of-credit";
252
+ }
253
+ if (/\b(over quota|quota exceeded|rate limit|too many requests|throttl)/.test(text)) {
254
+ return "engine-over-quota";
255
+ }
256
+ return undefined;
257
+ }
258
+
259
+ function firstLine(text: string): string {
260
+ return text.split("\n").find((line) => line.trim().length > 0)?.trim() ?? "";
261
+ }
262
+
263
+ /**
264
+ * Everything wrong with one figure, as readable field paths. Empty means the
265
+ * figure is usable.
266
+ *
267
+ * Pure and exported so the shapes an engine can get wrong are testable without
268
+ * a subprocess. Every check here mirrors one `validateBehaviourBlock` applies
269
+ * downstream (`packages/core/src/behaviour.ts`) — the difference is *when*:
270
+ * that one throws, and a throw is the whole-lexicon failure `lexicon.ts`
271
+ * reserves for a live credential. A third party emitting one bad number is not
272
+ * that, and reporting it as that is how an operator goes looking for a leak.
273
+ */
274
+ export function figureProblems(name: string, figure: unknown): string[] {
275
+ const at = (field: string) => `figures.${name}.${field}`;
276
+ if (typeof figure !== "object" || figure === null || Array.isArray(figure)) {
277
+ return [`${at("")} is ${Array.isArray(figure) ? "an array" : typeof figure}, not an object`];
278
+ }
279
+ const f = figure as Record<string, unknown>;
280
+ const out: string[] = [];
281
+
282
+ const fraction = (field: string, value: unknown) => {
283
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
284
+ out.push(`${at(field)} is not a number in 0..1`);
285
+ }
286
+ };
287
+
288
+ if (typeof f.perHour !== "number" || !Number.isFinite(f.perHour) || f.perHour < 0) {
289
+ out.push(`${at("perHour")} is not a non-negative finite number`);
290
+ }
291
+ if (typeof f.currency !== "string" || f.currency.trim().length === 0) {
292
+ out.push(`${at("currency")} is empty`);
293
+ }
294
+ fraction("errorRate", f.errorRate);
295
+
296
+ const headroom = f.headroom;
297
+ if (typeof headroom !== "object" || headroom === null || Array.isArray(headroom)) {
298
+ out.push(`${at("headroom")} is missing`);
299
+ } else {
300
+ const h = headroom as Record<string, unknown>;
301
+ // At least one axis, and an axis the engine did not model must be absent
302
+ // rather than zero — zero headroom means saturated, the opposite claim.
303
+ if (h.cpu === undefined && h.latency === undefined) {
304
+ out.push(`${at("headroom")} carries neither cpu nor latency`);
305
+ }
306
+ for (const axis of ["cpu", "latency"] as const) {
307
+ if (h[axis] !== undefined) fraction(`headroom.${axis}`, h[axis]);
308
+ }
309
+ }
310
+
311
+ const resilience = f.resilience;
312
+ if (typeof resilience !== "object" || resilience === null || Array.isArray(resilience)) {
313
+ out.push(`${at("resilience")} is missing`);
314
+ } else {
315
+ const r = resilience as Record<string, unknown>;
316
+ if (typeof r.failure !== "string" || r.failure.trim().length === 0) {
317
+ out.push(`${at("resilience.failure")} names no failure`);
318
+ }
319
+ if (!isResilienceVerdict(r.verdict)) {
320
+ out.push(`${at("resilience.verdict")} is not survives/degrades/fails`);
321
+ }
322
+ if (r.note !== undefined && typeof r.note !== "string") {
323
+ out.push(`${at("resilience.note")} is not a string`);
324
+ }
325
+ }
326
+
327
+ const rightSize = f.rightSize;
328
+ if (rightSize !== undefined) {
329
+ if (typeof rightSize !== "object" || rightSize === null || Array.isArray(rightSize)) {
330
+ out.push(`${at("rightSize")} is not an object`);
331
+ } else if (typeof (rightSize as Record<string, unknown>).suggestion !== "string") {
332
+ out.push(`${at("rightSize.suggestion")} is missing`);
333
+ }
334
+ }
335
+
336
+ return out;
337
+ }
338
+
339
+ /**
340
+ * What the parse of an engine's text comes to: the answer, or a detail
341
+ * naming what was wrong with it. Not a refusal yet — {@link transportEngine}
342
+ * builds that, with the endpoint the parse does not need to know.
343
+ */
344
+ export type ParsedEngineAnswer = { ok: true; answer: EngineAnswer } | { ok: false; detail: string };
345
+
346
+ /** One malformed verdict, with the detail the refusal will scrub and print. */
347
+ function malformed(detail: string): ParsedEngineAnswer {
348
+ return { ok: false, detail };
349
+ }
350
+
351
+ /**
352
+ * Parse an engine's text into an {@link EngineAnswer}.
353
+ *
354
+ * A malformed answer is `engine-unreachable` and not a report full of holes.
355
+ * The alternative — taking the fields that parsed and reporting the rest as
356
+ * unpredicted — would turn an engine emitting garbage into an estate that
357
+ * looks partially free, which is the failure the refusal arm exists to
358
+ * prevent, one level down.
359
+ *
360
+ * **Every figure is validated here, not only the envelope.** The first version
361
+ * of this function checked `engine`/`version`/`tolerance`/`basis` and that
362
+ * `figures` was an object, and then handed each figure's contents straight to
363
+ * `block()` in `./predict-behaviour.ts`, which dereferenced
364
+ * `figure.resilience.failure`. Seven malformed shapes were executed against
365
+ * it and all seven threw: three as bare `TypeError`s with no chant message,
366
+ * four through `validateBehaviourBlock` after the fact. A throw is the
367
+ * whole-lexicon failure `lexicon.ts` reserves for a live credential in the
368
+ * request, so a third-party engine emitting one bad number was indistinguishable
369
+ * from a leak — and the doc above claimed the opposite was happening.
370
+ *
371
+ * The detail names the entity and the field, because "the engine sent
372
+ * something wrong" is not a thing anybody can act on and "figures.web.headroom
373
+ * carries neither cpu nor latency" is. It flows into
374
+ * `unreachableBehaviourEngineRefusal`, which runs it through
375
+ * `scrubEngineDetail` and bounds it.
376
+ */
377
+ export function parseEngineAnswer(text: string): ParsedEngineAnswer {
378
+ let parsed: unknown;
379
+ try {
380
+ parsed = JSON.parse(text);
381
+ } catch {
382
+ return malformed(`the engine wrote ${text.length} byte(s) that are not JSON`);
383
+ }
384
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
385
+ return malformed("the engine's answer is not an object");
386
+ }
387
+ const answer = parsed as Partial<EngineAnswer>;
388
+
389
+ const missing = (["engine", "version", "tolerance"] as const).filter(
390
+ (key) => typeof answer[key] !== "string" || (answer[key] as string).trim().length === 0,
391
+ );
392
+ if (missing.length > 0) {
393
+ return malformed(
394
+ `the engine's answer states no ${missing.join(", ")}. Every figure carries provenance, so an ` +
395
+ "answer that cannot say who produced it is not a usable answer",
396
+ );
397
+ }
398
+ // A closed enum downstream, so a free-form basis is caught here rather than
399
+ // by a throw from `validateBehaviourBlock` after the report is half built.
400
+ if (!isBehaviourBasis(answer.basis)) {
401
+ return malformed(
402
+ `the engine's answer states a basis of ${JSON.stringify(answer.basis)}, which is not ` +
403
+ "modeled or validated",
404
+ );
405
+ }
406
+
407
+ if (typeof answer.figures !== "object" || answer.figures === null || Array.isArray(answer.figures)) {
408
+ // `Array.isArray` explicitly: `typeof [] === "object"`, so `figures: []`
409
+ // parsed clean and produced a report in which every node had been lost.
410
+ return malformed("the engine's answer carries no figures map");
411
+ }
412
+
413
+ if (answer.total !== undefined) {
414
+ const total = answer.total as Record<string, unknown>;
415
+ if (
416
+ typeof total !== "object" ||
417
+ total === null ||
418
+ typeof total.perHour !== "number" ||
419
+ !Number.isFinite(total.perHour) ||
420
+ total.perHour < 0 ||
421
+ typeof total.currency !== "string" ||
422
+ total.currency.trim().length === 0
423
+ ) {
424
+ return malformed("the engine's answer states a total that is not a non-negative rate in a named currency");
425
+ }
426
+ }
427
+
428
+ if (answer.declined !== undefined) {
429
+ const declined = answer.declined as unknown;
430
+ if (typeof declined !== "object" || declined === null || Array.isArray(declined)) {
431
+ return malformed("the engine's answer carries a declined list that is not a map");
432
+ }
433
+ for (const [name, reason] of Object.entries(declined as Record<string, unknown>)) {
434
+ if (typeof reason !== "string") {
435
+ return malformed(`the engine declined ${name} with a reason that is not a string`);
436
+ }
437
+ }
438
+ }
439
+
440
+ const problems: string[] = [];
441
+ for (const [name, figure] of Object.entries(answer.figures as Record<string, unknown>)) {
442
+ problems.push(...figureProblems(name, figure));
443
+ if (problems.length >= 5) break;
444
+ }
445
+ if (problems.length > 0) {
446
+ return malformed(
447
+ `the engine's answer is malformed: ${problems.slice(0, 5).join("; ")}` +
448
+ (problems.length >= 5 ? " (and possibly more)" : ""),
449
+ );
450
+ }
451
+
452
+ return { ok: true, answer: answer as EngineAnswer };
453
+ }
454
+
455
+ /**
456
+ * The transport chooser, with what the HTTP transport needs injectable.
457
+ *
458
+ * A `http://` or `https://` address gets core's `httpBehaviourTransport`,
459
+ * with its token read from `env` — the same `env` the address was read from,
460
+ * so a test drives both chains from one object. A bare address gets
461
+ * {@link commandTransport}. Any other scheme (`grpc://`, `unix://`) gets
462
+ * `undefined`, and the caller refuses naming the address: no transport here
463
+ * speaks it, and saying so beats a guess.
464
+ */
465
+ export function connectWith(deps: HttpBehaviourTransportDeps = {}): EngineConnect {
466
+ return (endpoint, env) => {
467
+ const address = endpoint.value.trim();
468
+ if (address.length === 0) return undefined;
469
+ if (isHttpBehaviourAddress(address)) {
470
+ return transportEngine(httpBehaviourTransport(BEHAVIOUR_SCOPE, endpoint, env, deps), endpoint);
471
+ }
472
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(address)) return undefined;
473
+ return commandEngine(endpoint);
474
+ };
475
+ }
476
+
477
+ /** The chooser the shipped plugin uses: the process's own `fetch`, the default deadline. */
478
+ export const defaultConnect: EngineConnect = connectWith();
@@ -82,7 +82,7 @@ export interface HttpBehaviourTransportDeps {
82
82
  timeoutMs?: number;
83
83
  }
84
84
 
85
- /** The default deadline: the same one augur's command transport gives a child. */
85
+ /** The default deadline: the same one the command transport gives a child. */
86
86
  export const HTTP_BEHAVIOUR_TIMEOUT_MS = 30_000;
87
87
 
88
88
  /** How much of an error body a `detail` repeats. `scrubEngineDetail` bounds it again downstream. */