@evoke-build/evoke 0.8.0 → 0.10.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.
- package/core.wasm +0 -0
- package/dist/contain.d.ts +3 -1
- package/dist/contain.js +2 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +5 -2
- package/dist/ops.d.ts +6 -2
- package/dist/project.d.ts +5 -0
- package/dist/project.js +61 -34
- package/dist/reflex.d.ts +25 -8
- package/dist/runtime.js +12 -7
- package/dist/types.d.ts +107 -14
- package/package.json +1 -1
- package/runtime/loader.mjs +20 -15
package/core.wasm
CHANGED
|
Binary file
|
package/dist/contain.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Contained, Facts, NeedsKey, Place, Policy, Program } from "./types.ts";
|
|
1
|
+
import type { Contained, Facts, NeedsKey, Place, Platform, Policy, Program } from "./types.ts";
|
|
2
|
+
/** This machine's platform, which the plan and the layers take. */
|
|
3
|
+
export declare const PLATFORM: Platform;
|
|
2
4
|
/** What the machine lacks that the declaration names: a path, or a program `PATH` does not hold. */
|
|
3
5
|
export type Missing = {
|
|
4
6
|
place: Place;
|
package/dist/contain.js
CHANGED
|
@@ -8,7 +8,8 @@ import { accessSync, closeSync, constants, mkdtempSync, openSync, readSync, real
|
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { call } from "./core.js";
|
|
11
|
-
|
|
11
|
+
/** This machine's platform, which the plan and the layers take. */
|
|
12
|
+
export const PLATFORM = process.platform === "linux" ? "linux" : "macos";
|
|
12
13
|
const SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
13
14
|
/** Whether this runtime's permission model holds the network: from Node 25, the first to know `--allow-net`. */
|
|
14
15
|
const HOLDS_NETWORK = process.allowedNodeEnvironmentFlags.has("--allow-net");
|
package/dist/errors.d.ts
CHANGED
|
@@ -22,11 +22,13 @@ export declare class FaultError extends EvokeError {
|
|
|
22
22
|
readonly fault: Fault;
|
|
23
23
|
constructor(fault: Fault, message: string, command: string);
|
|
24
24
|
}
|
|
25
|
-
/** A host or a body failed: what was attempted, why, and what to do; a body's own error is the `cause
|
|
25
|
+
/** A host or a body failed: what was attempted, why, and what to do; a body's own error is the `cause`, and the
|
|
26
|
+
* frames of an error a file body threw are `frames`, from the first. */
|
|
26
27
|
export declare class FailureError extends EvokeError {
|
|
27
28
|
readonly kind = "failure";
|
|
28
29
|
readonly what: string;
|
|
29
30
|
readonly why: string | undefined;
|
|
30
31
|
readonly fix: Fix;
|
|
31
|
-
|
|
32
|
+
readonly frames: readonly string[];
|
|
33
|
+
constructor(what: string, why: string | undefined, fix: Fix, command: string, options?: ErrorOptions, frames?: readonly string[]);
|
|
32
34
|
}
|
package/dist/errors.js
CHANGED
|
@@ -30,17 +30,20 @@ export class FaultError extends EvokeError {
|
|
|
30
30
|
this.fault = fault;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
/** A host or a body failed: what was attempted, why, and what to do; a body's own error is the `cause
|
|
33
|
+
/** A host or a body failed: what was attempted, why, and what to do; a body's own error is the `cause`, and the
|
|
34
|
+
* frames of an error a file body threw are `frames`, from the first. */
|
|
34
35
|
export class FailureError extends EvokeError {
|
|
35
36
|
kind = "failure";
|
|
36
37
|
what;
|
|
37
38
|
why;
|
|
38
39
|
fix;
|
|
39
|
-
|
|
40
|
+
frames;
|
|
41
|
+
constructor(what, why, fix, command, options, frames = []) {
|
|
40
42
|
super(`${what}${why === undefined ? "" : `: ${why}`} → ${command}`, command, options);
|
|
41
43
|
this.what = what;
|
|
42
44
|
this.why = why;
|
|
43
45
|
this.fix = fix;
|
|
46
|
+
this.frames = frames;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
/** The CLI's line for one problem. */
|
package/dist/ops.d.ts
CHANGED
|
@@ -109,6 +109,7 @@ export interface Ops {
|
|
|
109
109
|
input: {
|
|
110
110
|
set: T.Installed;
|
|
111
111
|
limits?: T.Limits;
|
|
112
|
+
platform?: T.Platform;
|
|
112
113
|
};
|
|
113
114
|
output: T.Result<T.Plan, T.Diagnostic>;
|
|
114
115
|
};
|
|
@@ -183,11 +184,12 @@ export interface Ops {
|
|
|
183
184
|
input: {
|
|
184
185
|
chosen: T.Chosen;
|
|
185
186
|
active: T.Active;
|
|
187
|
+
taken?: Record<T.ArgName, T.Json>;
|
|
186
188
|
input: T.Input;
|
|
187
189
|
deadline: T.Millis;
|
|
188
190
|
home: string;
|
|
189
191
|
};
|
|
190
|
-
output: T.Envelope
|
|
192
|
+
output: T.Result<T.Envelope, T.Diagnostic>;
|
|
191
193
|
};
|
|
192
194
|
argv: {
|
|
193
195
|
input: {
|
|
@@ -368,7 +370,8 @@ export interface Ops {
|
|
|
368
370
|
thieves: {
|
|
369
371
|
input: {
|
|
370
372
|
newcomers: T.LocalName[];
|
|
371
|
-
routed:
|
|
373
|
+
routed: T.Routed[];
|
|
374
|
+
floor?: T.Prob;
|
|
372
375
|
};
|
|
373
376
|
output: T.Theft[];
|
|
374
377
|
};
|
|
@@ -394,6 +397,7 @@ export interface Ops {
|
|
|
394
397
|
"weave.plan": {
|
|
395
398
|
input: {
|
|
396
399
|
plan: T.Plan;
|
|
400
|
+
gate?: T.Gate;
|
|
397
401
|
input: string;
|
|
398
402
|
tags: T.Tag[];
|
|
399
403
|
answers: T.Answers;
|
package/dist/project.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface LoadOptions<R = AnyReflexes> {
|
|
|
14
14
|
} | undefined;
|
|
15
15
|
/** Who answers. Absent: the adapter evoke.toml names, from its own subpath; required without a root. */
|
|
16
16
|
adapter?: Adapter | undefined;
|
|
17
|
+
/** The platform a manifest's `platforms` is judged by: this machine's when absent. A reflex named for another
|
|
18
|
+
* is inactive; name one to decide or test as that machine would. A body still runs here. */
|
|
19
|
+
platform?: W.Platform | undefined;
|
|
17
20
|
}
|
|
18
21
|
export interface DecideOptions {
|
|
19
22
|
/** Only reflexes carrying one of these tags are offered. */
|
|
@@ -129,6 +132,7 @@ export declare function load<R extends object = AnyReflexes>(options: {
|
|
|
129
132
|
[K in keyof R]?: Inline<R[K]> | undefined;
|
|
130
133
|
}> | undefined;
|
|
131
134
|
adapter?: Adapter | undefined;
|
|
135
|
+
platform?: W.Platform | undefined;
|
|
132
136
|
}): Promise<Project<R>>;
|
|
133
137
|
/** A project from reflexes handed as code alone: R is inferred from them. */
|
|
134
138
|
export declare function load<R extends object = AnyReflexes>(options: {
|
|
@@ -137,4 +141,5 @@ export declare function load<R extends object = AnyReflexes>(options: {
|
|
|
137
141
|
[K in keyof R]?: Inline<R[K]> | undefined;
|
|
138
142
|
} | undefined;
|
|
139
143
|
adapter: Adapter;
|
|
144
|
+
platform?: W.Platform | undefined;
|
|
140
145
|
}): Promise<Project<R>>;
|
package/dist/project.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { answered } from "./adapter.js";
|
|
10
|
-
import { facts, scratch, status } from "./contain.js";
|
|
10
|
+
import { PLATFORM, facts, scratch, status } from "./contain.js";
|
|
11
11
|
import { bug, call, command, fromCode, misnamed, problem, reply } from "./core.js";
|
|
12
12
|
import { DiagnosticError, FailureError } from "./errors.js";
|
|
13
13
|
import { entry, snapshot, text } from "./files.js";
|
|
@@ -93,7 +93,8 @@ export async function load(options) {
|
|
|
93
93
|
vocab[name] = call("vocabulary", { doc: { file: { type: "vocab", name }, toml } }, "load()");
|
|
94
94
|
}
|
|
95
95
|
const installed = { reflexes, vocab, adapter: adapter.id, evoke: call("version", {}) };
|
|
96
|
-
|
|
96
|
+
const platform = options.platform ?? PLATFORM;
|
|
97
|
+
return make({ installed, adapter, platform, dirs, bodies, shipped, local }, "load()");
|
|
97
98
|
}
|
|
98
99
|
/** The adapter evoke.toml names, from its own subpath; a recording is never resolved by name. */
|
|
99
100
|
async function named(project, root) {
|
|
@@ -169,17 +170,19 @@ function shown(input) {
|
|
|
169
170
|
function key(asked) {
|
|
170
171
|
return JSON.stringify([asked.text, asked.tags ?? [], asked.only ?? null]);
|
|
171
172
|
}
|
|
172
|
-
/** What the planner asked to decide a step, as its repair
|
|
173
|
-
* spliced into its words, was decided under that reflex alone
|
|
173
|
+
/** What the planner asked to decide a step, as its repair and its shared words tell: a fragment narrowed to its
|
|
174
|
+
* neighbour's reflex, or spliced into its words, was decided under that reflex alone, and so were words a shared word
|
|
175
|
+
* was written into; any other step over the tags. */
|
|
174
176
|
function askedFor(step, tags) {
|
|
175
|
-
const
|
|
177
|
+
const narrowed = step.repair === "narrowed" || step.repair === "spliced" || Object.values(step.shared ?? {}).some(shared => shared.via === "rewrite");
|
|
178
|
+
const own = narrowed ? step.reflex : undefined;
|
|
176
179
|
return own === undefined ? { text: step.text, tags } : { text: step.text, only: own };
|
|
177
180
|
}
|
|
178
181
|
/** The project over its ground: the set compiled, every reflex's status read off the plan. The implementation
|
|
179
182
|
* speaks the wire's shapes; the app's R lives on the interface alone. */
|
|
180
183
|
function make(ground, invoked) {
|
|
181
|
-
const { installed, adapter, dirs, bodies, shipped, local } = ground;
|
|
182
|
-
const plan = call("compile", { set: installed, ...(adapter.limits === undefined ? {} : { limits: adapter.limits }) }, invoked);
|
|
184
|
+
const { installed, adapter, platform, dirs, bodies, shipped, local } = ground;
|
|
185
|
+
const plan = call("compile", { set: installed, platform, ...(adapter.limits === undefined ? {} : { limits: adapter.limits }) }, invoked);
|
|
183
186
|
if (adapter.plan !== undefined && adapter.plan !== plan.digest) {
|
|
184
187
|
const message = `the recording was made against plan ${adapter.plan}, not ${plan.digest}`;
|
|
185
188
|
throw new DiagnosticError([{ message, fix: { type: "rerun" }, command: "replay(file, { record: jev() })" }]);
|
|
@@ -248,27 +251,18 @@ function make(ground, invoked) {
|
|
|
248
251
|
}
|
|
249
252
|
if (outcome !== "run" && outcome !== "confirm")
|
|
250
253
|
throw new TypeError(`a ${outcome} decision cannot run`);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const spent = chosen.trace.reduce((sum, entry) => sum + entry.ms, 0);
|
|
255
|
-
// The whole decision crosses: the core reads the fields of a Chosen and ignores the SDK's own.
|
|
256
|
-
const wire = chosen;
|
|
257
|
-
const envelope = call("envelope", { chosen: wire, active, input: chosen.input, deadline: Math.max(plan.deadline - spent, 0), home: homedir() });
|
|
258
|
-
const what = `running ${chosen.reflex}`;
|
|
259
|
-
const body = bodies[chosen.reflex];
|
|
260
|
-
if (body !== undefined)
|
|
261
|
-
return inline(what, body, envelope, options.signal);
|
|
262
|
-
const dir = dirs[chosen.reflex];
|
|
263
|
-
if (dir === undefined)
|
|
264
|
-
throw refused(chosen.reflex, `${chosen.reflex} has no body to run`, { type: "sync" }, "run(d)");
|
|
265
|
-
return contained(what, chosen, active, dir, envelope, options.signal);
|
|
254
|
+
// A whole result is handed by the plan alone: a call that takes one runs only as a step of a weave.
|
|
255
|
+
taker(chosen);
|
|
256
|
+
return running(chosen, {}, options);
|
|
266
257
|
},
|
|
267
258
|
async handle(input, options = {}) {
|
|
268
259
|
nonEmpty(input, "handle");
|
|
269
|
-
const
|
|
260
|
+
const decision = await project.decide(input, options);
|
|
261
|
+
if (decision.outcome !== "abstain")
|
|
262
|
+
taker(decision);
|
|
263
|
+
const readied = await ready(decision, options);
|
|
270
264
|
if (readied.ready)
|
|
271
|
-
return { outcome: "ran", decision: readied.decision, result: await
|
|
265
|
+
return { outcome: "ran", decision: readied.decision, result: await running(readied.decision, {}, signalled(options.signal)) };
|
|
272
266
|
if (readied.status === "refused")
|
|
273
267
|
return { outcome: "abstained", decision: readied.decision };
|
|
274
268
|
return { outcome: readied.status, decision: readied.decision };
|
|
@@ -308,7 +302,7 @@ function make(ground, invoked) {
|
|
|
308
302
|
async function planned(input, options, answers, traces) {
|
|
309
303
|
const invoked = `steps(${JSON.stringify(shown(input))})`;
|
|
310
304
|
for (;;) {
|
|
311
|
-
const planning = call("weave.plan", { plan, input, tags: options.tags ?? [], answers }, invoked);
|
|
305
|
+
const planning = call("weave.plan", { plan, ...gate, input, tags: options.tags ?? [], answers }, invoked);
|
|
312
306
|
if (planning.type === "done")
|
|
313
307
|
return planning.weave;
|
|
314
308
|
const { need } = planning;
|
|
@@ -446,7 +440,7 @@ function make(ground, invoked) {
|
|
|
446
440
|
record(handling, readied.decision, { status: readied.status, why: readied.why });
|
|
447
441
|
continue;
|
|
448
442
|
}
|
|
449
|
-
bodies.push(ran(readied.decision, signal).then(became => record(handling, readied.decision, became)));
|
|
443
|
+
bodies.push(ran(readied.decision, handling.taken ?? {}, signal).then(became => record(handling, readied.decision, became)));
|
|
450
444
|
}
|
|
451
445
|
await Promise.all(bodies);
|
|
452
446
|
}
|
|
@@ -506,16 +500,40 @@ function make(ground, invoked) {
|
|
|
506
500
|
...(ask === undefined ? {} : { ask: (decision) => ask(decision, turn) }),
|
|
507
501
|
};
|
|
508
502
|
}
|
|
509
|
-
/** A
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
503
|
+
/** A reflex that takes a whole result, met outside a weave: refused before any confirm, since only a request of
|
|
504
|
+
* several steps hands one. */
|
|
505
|
+
function taker(decision) {
|
|
506
|
+
const takes = Object.values(plan.active[decision.reflex]?.takes ?? {});
|
|
507
|
+
if (takes.length === 0)
|
|
508
|
+
return;
|
|
509
|
+
throw refused(decision.reflex, `${decision.reflex} takes ${words(takes)}, which a step before it in the same request returns`, { type: "rerun" }, 'weave("<input>")');
|
|
513
510
|
}
|
|
514
|
-
/**
|
|
515
|
-
*
|
|
516
|
-
|
|
511
|
+
/** The chosen call's body — in-process, in a child through the loader, or as an argv — with the whole results it
|
|
512
|
+
* takes beside its values, which only the weave hands; one the plan did not hand is refused by the envelope, the
|
|
513
|
+
* last guard. What `run` does for a call that takes none, and every step of a weave for its round. */
|
|
514
|
+
async function running(decision, taken, options) {
|
|
515
|
+
const chosen = own(decision, "run");
|
|
516
|
+
const active = plan.active[chosen.reflex];
|
|
517
|
+
if (active === undefined)
|
|
518
|
+
throw refused(chosen.reflex, `${chosen.reflex} is not active`, { type: "rerun" }, "run(d)");
|
|
519
|
+
const spent = chosen.trace.reduce((sum, entry) => sum + entry.ms, 0);
|
|
520
|
+
// The whole decision crosses: the core reads the fields of a Chosen and ignores the SDK's own.
|
|
521
|
+
const wire = chosen;
|
|
522
|
+
const envelope = call("envelope", { chosen: wire, active, taken, input: chosen.input, deadline: Math.max(plan.deadline - spent, 0), home: homedir() }, "run(d)");
|
|
523
|
+
const what = `running ${chosen.reflex}`;
|
|
524
|
+
const body = bodies[chosen.reflex];
|
|
525
|
+
if (body !== undefined)
|
|
526
|
+
return inline(what, body, envelope, options.signal);
|
|
527
|
+
const dir = dirs[chosen.reflex];
|
|
528
|
+
if (dir === undefined)
|
|
529
|
+
throw refused(chosen.reflex, `${chosen.reflex} has no body to run`, { type: "sync" }, "run(d)");
|
|
530
|
+
return contained(what, chosen, active, dir, envelope, options.signal);
|
|
531
|
+
}
|
|
532
|
+
/** One body run for a weave, the whole results its step takes beside it: what it returned, or its failure as the
|
|
533
|
+
* step's own outcome, never the weave's; a body the signal ended is the round cancelled. */
|
|
534
|
+
async function ran(decision, taken, signal) {
|
|
517
535
|
try {
|
|
518
|
-
return { status: "ran", result: await
|
|
536
|
+
return { status: "ran", result: await running(decision, taken, signalled(signal)) };
|
|
519
537
|
}
|
|
520
538
|
catch (error) {
|
|
521
539
|
if (signal?.aborted && error === signal.reason)
|
|
@@ -604,6 +622,15 @@ function carrying(reason, woven) {
|
|
|
604
622
|
}
|
|
605
623
|
return reason;
|
|
606
624
|
}
|
|
625
|
+
/** A signal as run options: none when there is none. */
|
|
626
|
+
function signalled(signal) {
|
|
627
|
+
return signal === undefined ? {} : { signal };
|
|
628
|
+
}
|
|
629
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
630
|
+
function words(names) {
|
|
631
|
+
const last = names.at(-1) ?? "";
|
|
632
|
+
return names.length < 2 ? last : `${names.slice(0, -1).join(", ")} and ${last}`;
|
|
633
|
+
}
|
|
607
634
|
/** Whether two asks want the same things for the same reasons. */
|
|
608
635
|
function same(after, before) {
|
|
609
636
|
return JSON.stringify(after) === JSON.stringify(before);
|
package/dist/reflex.d.ts
CHANGED
|
@@ -10,17 +10,21 @@ export interface InlineManifest {
|
|
|
10
10
|
effect?: Effect;
|
|
11
11
|
/** The one-line template a person confirms, naming required arguments only. */
|
|
12
12
|
confirm: string;
|
|
13
|
+
/** The arguments: a question and one source each, or a whole result an earlier step returns, taken by its name. */
|
|
13
14
|
args?: Record<string, InlineArg>;
|
|
14
15
|
/** What the body's `data` yields for a later step to take: per field, the recognizer that reads it, or a list of
|
|
15
16
|
* records with such fields. */
|
|
16
17
|
yields?: Record<string, Recognizer | {
|
|
17
18
|
each: Record<string, Recognizer>;
|
|
18
19
|
}>;
|
|
20
|
+
/** The name the body's whole `data` goes by, for a later step to take. */
|
|
21
|
+
returns?: string;
|
|
19
22
|
examples?: InlineRecords;
|
|
20
23
|
tests?: InlineRecords;
|
|
21
24
|
}
|
|
22
|
-
/** An argument: its question and exactly one source
|
|
23
|
-
|
|
25
|
+
/** An argument: its question and exactly one source, a flag optional by nature; or `takes` alone, a whole result
|
|
26
|
+
* an earlier step returns, by the name that result goes by — filled by the plan, never asked. */
|
|
27
|
+
export type InlineArg = ({
|
|
24
28
|
ask: string;
|
|
25
29
|
} & ({
|
|
26
30
|
options: Record<string, string>;
|
|
@@ -37,7 +41,9 @@ export type InlineArg = {
|
|
|
37
41
|
optional?: boolean;
|
|
38
42
|
} | {
|
|
39
43
|
flag: true;
|
|
40
|
-
})
|
|
44
|
+
})) | {
|
|
45
|
+
takes: string;
|
|
46
|
+
};
|
|
41
47
|
/** Utterances with what they assert: a record per argument — the text, or `false` for unstated, `true` for a flag —
|
|
42
48
|
* or `false` for never this reflex. */
|
|
43
49
|
export type InlineRecords = Record<string, Record<string, string | boolean> | false>;
|
|
@@ -57,23 +63,34 @@ type Absent<A> = A extends {
|
|
|
57
63
|
} | {
|
|
58
64
|
flag: true;
|
|
59
65
|
} ? true : false;
|
|
66
|
+
type Taken<A> = A extends {
|
|
67
|
+
takes: string;
|
|
68
|
+
} ? true : false;
|
|
60
69
|
type Flat<T> = {
|
|
61
70
|
[K in keyof T]: T[K];
|
|
62
71
|
} & {};
|
|
63
72
|
type ArgsOf<M extends InlineManifest> = NonNullable<M["args"]> extends Record<string, InlineArg> ? NonNullable<M["args"]> : Record<never, InlineArg>;
|
|
64
|
-
/**
|
|
73
|
+
/** The asked arguments alone: a taken one never travels in a decision. */
|
|
74
|
+
type AskedOf<M extends InlineManifest> = {
|
|
75
|
+
[N in keyof ArgsOf<M> as Taken<ArgsOf<M>[N]> extends true ? never : N]: ArgsOf<M>[N];
|
|
76
|
+
};
|
|
77
|
+
/** What a decision carries for a reflex handed as code: its asked arguments as the wire types them, optional ones
|
|
78
|
+
* optional. */
|
|
65
79
|
export type Carried<M extends InlineManifest> = Flat<{
|
|
66
|
-
[N in keyof
|
|
80
|
+
[N in keyof AskedOf<M> as Absent<AskedOf<M>[N]> extends true ? never : N]: Typed<AskedOf<M>[N]>;
|
|
67
81
|
} & {
|
|
68
|
-
[N in keyof
|
|
82
|
+
[N in keyof AskedOf<M> as Absent<AskedOf<M>[N]> extends true ? N : never]?: Typed<AskedOf<M>[N]>;
|
|
69
83
|
}>;
|
|
70
84
|
/** What decisions carry for a map of reflexes handed as code: `load<Reflexes & ReflexesOf<typeof own>>` when a
|
|
71
85
|
* project has installed reflexes beside them. */
|
|
72
86
|
export type ReflexesOf<I> = {
|
|
73
87
|
[K in keyof I]: I[K] extends Inline<infer S> ? S : never;
|
|
74
88
|
};
|
|
75
|
-
/** The body's arguments, plain: what `reflex`'s body receives
|
|
76
|
-
|
|
89
|
+
/** The body's arguments, plain: what `reflex`'s body receives — the asked arguments' values, and each whole result
|
|
90
|
+
* it takes as `unknown`, since evoke checks no shape. */
|
|
91
|
+
export type Args<M extends InlineManifest> = Flat<Values<Carried<M>> & {
|
|
92
|
+
[N in keyof ArgsOf<M> as Taken<ArgsOf<M>[N]> extends true ? N : never]: unknown;
|
|
93
|
+
}>;
|
|
77
94
|
/** What `reflex` returns and `load` takes; `shape` never holds a value — it is what a decision carries, for inference. */
|
|
78
95
|
export interface Inline<S = Record<string, Value>> {
|
|
79
96
|
readonly manifest: InlineManifest;
|
package/dist/runtime.js
CHANGED
|
@@ -25,13 +25,17 @@ const LOADER = fileURLToPath(new URL("../runtime/loader.mjs", import.meta.url));
|
|
|
25
25
|
const KEPT = ["PATH", "HOME", "TMPDIR", "LANG", "TERM"];
|
|
26
26
|
/** What a body gets to settle after its signal aborts, and a group after SIGTERM before SIGKILL, in milliseconds. */
|
|
27
27
|
const GRACE = 1000;
|
|
28
|
+
/** Milliseconds as a person reads them: `30 s`, `1.5 s`, `0.05 s`. */
|
|
29
|
+
function seconds(ms) {
|
|
30
|
+
return `${ms < 1000 ? (ms / 1000).toFixed(2) : Math.round(ms / 100) / 10} s`;
|
|
31
|
+
}
|
|
28
32
|
/** A function body, in-process: its arguments as the envelope carries them, the deadline as its signal. */
|
|
29
33
|
export async function inline(what, body, envelope, signal) {
|
|
30
34
|
signal?.throwIfAborted();
|
|
31
35
|
const config = resolved(what, envelope.config);
|
|
32
36
|
// Referenced timers: a body that hangs holding no handle cannot let the process exit before the deadline.
|
|
33
37
|
const timeout = new AbortController();
|
|
34
|
-
const timer = setTimeout(() => timeout.abort(new DOMException(`no answer within ${envelope.deadline}
|
|
38
|
+
const timer = setTimeout(() => timeout.abort(new DOMException(`no answer within ${seconds(envelope.deadline)}`, "TimeoutError")), envelope.deadline);
|
|
35
39
|
const own = signal === undefined ? timeout.signal : AbortSignal.any([signal, timeout.signal]);
|
|
36
40
|
let grace;
|
|
37
41
|
const abandoned = new Promise(resolve => {
|
|
@@ -49,7 +53,7 @@ export async function inline(what, body, envelope, signal) {
|
|
|
49
53
|
if (signal?.aborted)
|
|
50
54
|
throw signal.reason;
|
|
51
55
|
if ("abandoned" in outcome)
|
|
52
|
-
throw failed(what, `did not finish within ${envelope.deadline}
|
|
56
|
+
throw failed(what, `did not finish within ${seconds(envelope.deadline)}`);
|
|
53
57
|
if ("threw" in outcome) {
|
|
54
58
|
throw failed(what, outcome.threw instanceof Error ? outcome.threw.message : String(outcome.threw), undefined, outcome.threw);
|
|
55
59
|
}
|
|
@@ -91,7 +95,7 @@ export async function child(what, dir, envelope, layers, signal) {
|
|
|
91
95
|
if (error !== undefined)
|
|
92
96
|
throw failed(what, error.message);
|
|
93
97
|
if (timedOut)
|
|
94
|
-
throw failed(what, `did not finish within ${envelope.deadline}
|
|
98
|
+
throw failed(what, `did not finish within ${seconds(envelope.deadline)}`);
|
|
95
99
|
const line = output.split("\n")[0] ?? "";
|
|
96
100
|
let parsed;
|
|
97
101
|
try {
|
|
@@ -105,7 +109,8 @@ export async function child(what, dir, envelope, layers, signal) {
|
|
|
105
109
|
if (refused !== null && typeof refused === "object" && "what" in refused && "path" in refused) {
|
|
106
110
|
throw new Refusal(String(parsed.error), { what: String(refused.what), path: String(refused.path) });
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const frames = "frames" in parsed && Array.isArray(parsed.frames) ? parsed.frames.map(String) : [];
|
|
113
|
+
throw failed(what, String(parsed.error), undefined, undefined, frames);
|
|
109
114
|
}
|
|
110
115
|
return result(what, parsed);
|
|
111
116
|
}
|
|
@@ -139,7 +144,7 @@ export async function program(what, argv, envelope, layers, signal) {
|
|
|
139
144
|
if (error !== undefined)
|
|
140
145
|
throw failed(what, error.message);
|
|
141
146
|
if (timedOut)
|
|
142
|
-
throw failed(what, `did not finish within ${envelope.deadline}
|
|
147
|
+
throw failed(what, `did not finish within ${seconds(envelope.deadline)}`);
|
|
143
148
|
if (code !== 0)
|
|
144
149
|
throw failed(what, refusedProfile(code) ?? ended(code));
|
|
145
150
|
return { text: output.endsWith("\n") ? output.slice(0, -1) : output };
|
|
@@ -249,6 +254,6 @@ function describe(value) {
|
|
|
249
254
|
function ended(code) {
|
|
250
255
|
return code === null ? "was killed" : `exited ${code}`;
|
|
251
256
|
}
|
|
252
|
-
function failed(what, why, fix = { type: "rerun" }, cause) {
|
|
253
|
-
return new FailureError(what, why, fix, command(fix, `run(d)`), cause === undefined ? undefined : { cause });
|
|
257
|
+
function failed(what, why, fix = { type: "rerun" }, cause, frames = []) {
|
|
258
|
+
return new FailureError(what, why, fix, command(fix, `run(d)`), cause === undefined ? undefined : { cause }, frames);
|
|
254
259
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -91,6 +91,13 @@ export type File = {
|
|
|
91
91
|
export type Fix = {
|
|
92
92
|
type: "vocab_add";
|
|
93
93
|
vocab: VocabName;
|
|
94
|
+
} | {
|
|
95
|
+
type: "vocab_value";
|
|
96
|
+
vocab: VocabName;
|
|
97
|
+
word: Word;
|
|
98
|
+
} | {
|
|
99
|
+
type: "make_dir";
|
|
100
|
+
path: string;
|
|
94
101
|
} | {
|
|
95
102
|
type: "vocab_remove";
|
|
96
103
|
vocab: VocabName;
|
|
@@ -172,12 +179,19 @@ export interface Manifest {
|
|
|
172
179
|
confirm: Template;
|
|
173
180
|
/** Absent: inline, a function the SDK holds. */
|
|
174
181
|
run?: Run;
|
|
182
|
+
/** Where the body runs, when not anywhere; elsewhere the reflex is inactive. Absent when anywhere. Contract, like `run`. */
|
|
183
|
+
platforms?: Platform[];
|
|
175
184
|
/** What the body may touch; absent, the tightest declaration. Contract, like `run`. */
|
|
176
185
|
needs?: Needs;
|
|
177
186
|
config: Record<ConfigKey, ConfigSpec>;
|
|
187
|
+
/** The arguments the classifier is asked about: `ask` and one source each. */
|
|
178
188
|
args: Record<ArgName, Argument>;
|
|
189
|
+
/** The arguments an earlier step's whole result fills, by the name that result goes by; absent when none. Contract. */
|
|
190
|
+
takes?: Record<ArgName, FieldName>;
|
|
179
191
|
/** What the body's `data` yields for a later step to take, per field. */
|
|
180
192
|
yields: Record<FieldName, Yield>;
|
|
193
|
+
/** The name the body's whole `data` goes by, for a later step to take; absent when none. Contract. */
|
|
194
|
+
returns?: FieldName;
|
|
181
195
|
examples: Records;
|
|
182
196
|
tests: Records;
|
|
183
197
|
/** Keys the format does not know: reported, never fatal. */
|
|
@@ -604,9 +618,14 @@ export interface Active {
|
|
|
604
618
|
/** Absent: the tightest declaration. */
|
|
605
619
|
needs?: Needs;
|
|
606
620
|
confirm: Template;
|
|
621
|
+
/** The arguments the classifier is asked about. */
|
|
607
622
|
args: Record<ArgName, Argument>;
|
|
623
|
+
/** The arguments an earlier step's whole result fills, by the name that result goes by; absent when none. */
|
|
624
|
+
takes?: Record<ArgName, FieldName>;
|
|
608
625
|
/** What the body's `data` yields for a later step to take, per field. */
|
|
609
626
|
yields: Record<FieldName, Yield>;
|
|
627
|
+
/** The name the body's whole `data` goes by, for a later step to take; absent when none. */
|
|
628
|
+
returns?: FieldName;
|
|
610
629
|
config: Record<ConfigKey, Setting>;
|
|
611
630
|
tags: Tag[];
|
|
612
631
|
}
|
|
@@ -640,7 +659,7 @@ export type PickValue = {
|
|
|
640
659
|
value: Clean;
|
|
641
660
|
};
|
|
642
661
|
/** What a request asks: everything, or the route alone — the conflict test at `add`. */
|
|
643
|
-
export type Scope = "full" | "route";
|
|
662
|
+
export type Scope = "full" | "route" | "fits";
|
|
644
663
|
/** What the answers said: the reflexes ranked, every choice read, and the winner with its values. */
|
|
645
664
|
export interface Reading {
|
|
646
665
|
/** Sorted by route probability; `try` prints it. */
|
|
@@ -799,8 +818,9 @@ export interface Envelope {
|
|
|
799
818
|
reflex: LocalName;
|
|
800
819
|
/** Absent for an inline body. */
|
|
801
820
|
run?: Run;
|
|
802
|
-
/** An option key, a word's `value` if set else the word, a pick's value, `true` for a flag
|
|
803
|
-
|
|
821
|
+
/** An option key, a word's `value` if set else the word, a pick's value, `true` for a flag, and a whole result as
|
|
822
|
+
* its source returned it. */
|
|
823
|
+
args: Record<ArgName, Json>;
|
|
804
824
|
input: Input;
|
|
805
825
|
/** An `env` setting stays a reference; the host resolves it. */
|
|
806
826
|
config: Record<ConfigKey, Setting>;
|
|
@@ -866,8 +886,16 @@ export interface Ref {
|
|
|
866
886
|
many?: boolean;
|
|
867
887
|
p?: Prob;
|
|
868
888
|
}
|
|
869
|
-
/** How a
|
|
870
|
-
|
|
889
|
+
/** How a step came to be that is not one part decided on its own: a segment that matched nothing settled narrowed to
|
|
890
|
+
* its neighbour's reflex, spliced into its words, or merged back; or a part the engine kept whole split, its parts each
|
|
891
|
+
* a reflex of their own. */
|
|
892
|
+
export type Repair = "narrowed" | "spliced" | "merged" | "split";
|
|
893
|
+
/** A word of a vocabulary the request stated for several steps, as it reached one of them: a required argument filled
|
|
894
|
+
* as a person's answer would fill it, or an optional one written into the step's words and decided again narrowed. */
|
|
895
|
+
export interface Shared {
|
|
896
|
+
word: Word;
|
|
897
|
+
via: Via;
|
|
898
|
+
}
|
|
871
899
|
/** One step of the plan: a segment's text and the foundation's decision on it, in the order it is to happen. */
|
|
872
900
|
export interface Step {
|
|
873
901
|
/** From 1, as the plan prints it. */
|
|
@@ -880,18 +908,24 @@ export interface Step {
|
|
|
880
908
|
effect?: Effect;
|
|
881
909
|
refs?: Ref[];
|
|
882
910
|
repair?: Repair;
|
|
911
|
+
/** The words the request stated once for several steps that reached this one's arguments, by argument. */
|
|
912
|
+
shared?: Record<ArgName, Shared>;
|
|
883
913
|
/** The steps this one must follow: an explicit `then`, or a binding. */
|
|
884
914
|
after?: number[];
|
|
885
915
|
}
|
|
886
|
-
/** How a bound value reaches its step: answering its own ask
|
|
887
|
-
|
|
888
|
-
|
|
916
|
+
/** How a bound value reaches its step: answering its own ask; the step decided again with the value in its words; or a
|
|
917
|
+
* whole result handed beside the decision, never through its words. */
|
|
918
|
+
export type Via = "fill" | "rewrite" | "takes";
|
|
919
|
+
/** A value of one step's result taken by a later step: which field, into which argument, how; or the whole result, by
|
|
920
|
+
* the name it goes by. */
|
|
889
921
|
export interface Binding {
|
|
890
922
|
from: number;
|
|
891
923
|
to: number;
|
|
892
924
|
arg: ArgName;
|
|
925
|
+
/** The field taken; for `takes`, the name the whole result goes by. */
|
|
893
926
|
field: FieldName;
|
|
894
|
-
|
|
927
|
+
/** The recognizer the field is read with; absent for a whole result. */
|
|
928
|
+
kind?: Recognizer;
|
|
895
929
|
via: Via;
|
|
896
930
|
/** The list field of the source's result whose records carry `field`: the step runs once per record. */
|
|
897
931
|
each?: FieldName;
|
|
@@ -920,6 +954,19 @@ export type Because = {
|
|
|
920
954
|
type: "takes_nothing";
|
|
921
955
|
step: number;
|
|
922
956
|
sources: number[];
|
|
957
|
+
}
|
|
958
|
+
/** A whole result the step takes by name that no step before it returns: refused, nothing answers it. */
|
|
959
|
+
| {
|
|
960
|
+
type: "no_source";
|
|
961
|
+
step: number;
|
|
962
|
+
name: FieldName;
|
|
963
|
+
}
|
|
964
|
+
/** What the step takes one of that several steps return, or one step once per record: refused. */
|
|
965
|
+
| {
|
|
966
|
+
type: "several_sources";
|
|
967
|
+
step: number;
|
|
968
|
+
name: FieldName;
|
|
969
|
+
sources: number[];
|
|
923
970
|
};
|
|
924
971
|
/** The verdict before anything runs, with every reason. */
|
|
925
972
|
export interface Verdict {
|
|
@@ -940,19 +987,20 @@ export interface Weave {
|
|
|
940
987
|
stages: number[][];
|
|
941
988
|
verdict: Verdict;
|
|
942
989
|
}
|
|
943
|
-
/** A value bound into a step at its turn. */
|
|
990
|
+
/** A value bound into a step at its turn; a whole result carries no value here, it stays on its source's line. */
|
|
944
991
|
export interface Bound {
|
|
945
992
|
arg: ArgName;
|
|
946
993
|
from: number;
|
|
947
994
|
field: FieldName;
|
|
948
|
-
value
|
|
995
|
+
value?: string;
|
|
949
996
|
}
|
|
950
997
|
/** What a body returned: its text, and data when it gave some. */
|
|
951
998
|
export interface Returned {
|
|
952
999
|
text: string;
|
|
953
1000
|
data?: Json;
|
|
954
1001
|
}
|
|
955
|
-
/** One round of a step for a host to take through the foundation's loop, the bound values in place
|
|
1002
|
+
/** One round of a step for a host to take through the foundation's loop, the bound values in place and the whole
|
|
1003
|
+
* results the body receives beside them. */
|
|
956
1004
|
export interface Handling {
|
|
957
1005
|
step: number;
|
|
958
1006
|
/** From 0; a step bound to a list runs one round per record. */
|
|
@@ -960,14 +1008,24 @@ export interface Handling {
|
|
|
960
1008
|
decision: Decision;
|
|
961
1009
|
input: string;
|
|
962
1010
|
bound?: Bound[];
|
|
1011
|
+
/** Per taken argument, its source's whole `data`: the same for every round of the step. */
|
|
1012
|
+
taken?: Record<ArgName, Json>;
|
|
963
1013
|
}
|
|
964
1014
|
/** What became of a step, or of one of its rounds. */
|
|
965
1015
|
export type Status = "ran" | "failed" | "declined" | "refused" | "skipped" | "unanswered";
|
|
966
1016
|
/** Why a step did not run, or did not finish. */
|
|
967
1017
|
export type WeaveWhy = {
|
|
968
1018
|
type: "earlier_step";
|
|
969
|
-
}
|
|
1019
|
+
}
|
|
1020
|
+
/** The step it names yielded nothing it can take: no such field, no data, or null. */
|
|
1021
|
+
| {
|
|
970
1022
|
type: "nothing_to_take";
|
|
1023
|
+
from: number;
|
|
1024
|
+
}
|
|
1025
|
+
/** The step it names returned a whole result over the cap, a mebibyte of JSON. */
|
|
1026
|
+
| {
|
|
1027
|
+
type: "too_large";
|
|
1028
|
+
from: number;
|
|
971
1029
|
} | {
|
|
972
1030
|
type: "found_nothing";
|
|
973
1031
|
} | {
|
|
@@ -1035,7 +1093,7 @@ export interface ContractDiff {
|
|
|
1035
1093
|
}
|
|
1036
1094
|
/** `same`: nothing but wording, or a declaration narrowed. `minor`: additions, a declaration widened, and a config key gone. `major`: something a person's files or calls may not survive. */
|
|
1037
1095
|
export type Level = "same" | "minor" | "major";
|
|
1038
|
-
/** One change to the contract, in the order the diff walks: the previous arguments, the added ones, the body, what it may touch, config,
|
|
1096
|
+
/** One change to the contract, in the order the diff walks: the previous arguments, the added ones, the body, what it may touch, config, what the result yields, what the reflex takes, then what it returns. */
|
|
1039
1097
|
export type Change = {
|
|
1040
1098
|
type: "arg_removed";
|
|
1041
1099
|
arg: ArgName;
|
|
@@ -1061,6 +1119,12 @@ export type Change = {
|
|
|
1061
1119
|
} | {
|
|
1062
1120
|
type: "needs_narrowed";
|
|
1063
1121
|
removed: Needs;
|
|
1122
|
+
} | {
|
|
1123
|
+
type: "platform_added";
|
|
1124
|
+
platform: Platform;
|
|
1125
|
+
} | {
|
|
1126
|
+
type: "platform_removed";
|
|
1127
|
+
platform: Platform;
|
|
1064
1128
|
} | {
|
|
1065
1129
|
type: "required";
|
|
1066
1130
|
arg: ArgName;
|
|
@@ -1093,6 +1157,27 @@ export type Change = {
|
|
|
1093
1157
|
} | {
|
|
1094
1158
|
type: "yield_changed";
|
|
1095
1159
|
field: FieldName;
|
|
1160
|
+
} | {
|
|
1161
|
+
type: "takes_added";
|
|
1162
|
+
arg: ArgName;
|
|
1163
|
+
name: FieldName;
|
|
1164
|
+
} | {
|
|
1165
|
+
type: "takes_removed";
|
|
1166
|
+
arg: ArgName;
|
|
1167
|
+
name: FieldName;
|
|
1168
|
+
} | {
|
|
1169
|
+
type: "takes_changed";
|
|
1170
|
+
arg: ArgName;
|
|
1171
|
+
name: FieldName;
|
|
1172
|
+
} | {
|
|
1173
|
+
type: "returns_added";
|
|
1174
|
+
name: FieldName;
|
|
1175
|
+
} | {
|
|
1176
|
+
type: "returns_removed";
|
|
1177
|
+
name: FieldName;
|
|
1178
|
+
} | {
|
|
1179
|
+
type: "returns_changed";
|
|
1180
|
+
name: FieldName;
|
|
1096
1181
|
};
|
|
1097
1182
|
/** `was` is flat and cumulative: a retired name never returns as a live argument, and never leaves the lists. */
|
|
1098
1183
|
export type WasViolation = {
|
|
@@ -1200,6 +1285,14 @@ export interface Theft {
|
|
|
1200
1285
|
phrase: Utterance;
|
|
1201
1286
|
owner: LocalName;
|
|
1202
1287
|
thief: LocalName;
|
|
1288
|
+
/** Present when the newcomer did not win the phrase but fits it at or over the floor. */
|
|
1289
|
+
fits?: Prob;
|
|
1290
|
+
}
|
|
1291
|
+
/** An installed case routed over the new set at `add`: who won, and every reflex offered with its `fits`. */
|
|
1292
|
+
export interface Routed {
|
|
1293
|
+
case: Case;
|
|
1294
|
+
winner?: LocalName;
|
|
1295
|
+
ranking?: Contender[];
|
|
1203
1296
|
}
|
|
1204
1297
|
/** The report `evoke calibrate` prints: every record of the active reflexes decided once, or `repeats` times, and judged; each input counts once, by its first decision, the repeats measuring stability. */
|
|
1205
1298
|
export interface Calibration {
|
package/package.json
CHANGED
package/runtime/loader.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// The loader: one envelope line on stdin — { run, args, input, config, deadline } — the body `run` names imported
|
|
2
2
|
// and its default export called with (args, { input, config, signal }), then one result line on stdout:
|
|
3
|
-
// { text, data? } or { error, refused? } — `refused` when the error is a refusal by Node's permission
|
|
4
|
-
// kernel: what was refused, a permission or a syscall, and the path, so the host names the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// { text, data? } or { error, refused?, frames? } — `refused` when the error is a refusal by Node's permission
|
|
4
|
+
// model or the kernel: what was refused, a permission or a syscall, and the path, so the host names the
|
|
5
|
+
// declaration's key; `frames` the frames of an error the body threw, without the message and without the frames
|
|
6
|
+
// inside Node itself, for the host to keep. It exits when stdin closes before a body, and during one aborts the
|
|
7
|
+
// body's `signal` first, so its life is bounded by its parent's and the body still hears the end. The body's
|
|
8
|
+
// console and stdout go to stderr; an error thrown from a callback ends the body as a rejection does. SIGTERM, the deadline and
|
|
9
9
|
// stdin's end abort `signal`; a body that has not settled a second later is abandoned. The SDK ships this same
|
|
10
10
|
// file.
|
|
11
11
|
import { writeSync } from "node:fs";
|
|
@@ -43,8 +43,8 @@ const out = (value) => {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
const fail = (message, refused) => {
|
|
47
|
-
out(refused === undefined ? {
|
|
46
|
+
const fail = (message, refused, frames) => {
|
|
47
|
+
out({ error: message, ...(refused === undefined ? {} : { refused }), ...(frames?.length ? { frames } : {}) });
|
|
48
48
|
process.exit(1);
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -76,7 +76,7 @@ async function run(line) {
|
|
|
76
76
|
const { run, args, input, config, deadline } = envelope;
|
|
77
77
|
process.on("SIGTERM", () => stop("terminated", true));
|
|
78
78
|
process.on("uncaughtException", thrown);
|
|
79
|
-
const timer = setTimeout(() => stop(`timed out after ${deadline}
|
|
79
|
+
const timer = setTimeout(() => stop(`timed out after ${seconds(deadline)}`, true), deadline);
|
|
80
80
|
try {
|
|
81
81
|
const module = await import(pathToFileURL(run).href);
|
|
82
82
|
if (module.default === undefined) throw new Error(`${run} has no default export`);
|
|
@@ -94,12 +94,11 @@ async function run(line) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// A body's error, whether its promise rejected with it or a callback threw it: the
|
|
98
|
-
//
|
|
97
|
+
// A body's error, whether its promise rejected with it or a callback threw it: the message, what refused the
|
|
98
|
+
// body, when something did, and its frames, as the one line.
|
|
99
99
|
function thrown(error) {
|
|
100
|
-
const frames = error instanceof Error && error.stack ? where(error.stack) :
|
|
101
|
-
|
|
102
|
-
fail(error instanceof Error ? error.message : String(error), refusal(error));
|
|
100
|
+
const frames = error instanceof Error && error.stack ? where(error.stack) : [];
|
|
101
|
+
fail(error instanceof Error ? error.message : String(error), refusal(error), frames);
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
// What refused the body, when something did. A lookup names the host, whether Node's permission model refused it
|
|
@@ -117,12 +116,18 @@ function refusal(error) {
|
|
|
117
116
|
return { what: text(e.syscall), path: text(e.path) || address };
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
// The frames of a stack, from the first, each without its indent; none from inside Node.
|
|
120
120
|
function where(stack) {
|
|
121
121
|
const lines = stack.split("\n");
|
|
122
122
|
const first = lines.findIndex((line) => line.startsWith(" at "));
|
|
123
123
|
return (first < 0 ? [] : lines.slice(first))
|
|
124
124
|
.filter((line) => !/[( ]node:/.test(line) && !line.includes("[eval"))
|
|
125
|
-
.
|
|
125
|
+
.map((line) => line.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Milliseconds as a person reads them: `30 s`, `1.5 s`, `0.05 s`.
|
|
129
|
+
function seconds(ms) {
|
|
130
|
+
return `${ms < 1000 ? (ms / 1000).toFixed(2) : Math.round(ms / 100) / 10} s`;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
function describe(value) {
|