@evoke-build/evoke 0.9.0 → 0.10.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/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
- const PLATFORM = process.platform === "linux" ? "linux" : "macos";
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
- constructor(what: string, why: string | undefined, fix: Fix, command: string, options?: ErrorOptions);
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
- constructor(what, why, fix, command, options) {
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
  };
@@ -369,7 +370,8 @@ export interface Ops {
369
370
  thieves: {
370
371
  input: {
371
372
  newcomers: T.LocalName[];
372
- routed: [T.Case, T.LocalName | null][];
373
+ routed: T.Routed[];
374
+ floor?: T.Prob;
373
375
  };
374
376
  output: T.Theft[];
375
377
  };
@@ -395,6 +397,7 @@ export interface Ops {
395
397
  "weave.plan": {
396
398
  input: {
397
399
  plan: T.Plan;
400
+ gate?: T.Gate;
398
401
  input: string;
399
402
  tags: T.Tag[];
400
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
- return make({ installed, adapter, dirs, bodies, shipped, local }, "load()");
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 tells: a fragment narrowed to its neighbour's reflex, or
173
- * spliced into its words, was decided under that reflex alone; any other step over the tags. */
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 own = step.repair === "narrowed" || step.repair === "spliced" ? step.reflex : undefined;
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() })" }]);
@@ -299,7 +302,7 @@ function make(ground, invoked) {
299
302
  async function planned(input, options, answers, traces) {
300
303
  const invoked = `steps(${JSON.stringify(shown(input))})`;
301
304
  for (;;) {
302
- 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);
303
306
  if (planning.type === "done")
304
307
  return planning.weave;
305
308
  const { need } = planning;
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} ms`, "TimeoutError")), 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} ms`);
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} ms`);
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
- throw failed(what, String(parsed.error));
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} ms`);
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,6 +179,8 @@ 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>;
@@ -650,7 +659,7 @@ export type PickValue = {
650
659
  value: Clean;
651
660
  };
652
661
  /** What a request asks: everything, or the route alone — the conflict test at `add`. */
653
- export type Scope = "full" | "route";
662
+ export type Scope = "full" | "route" | "fits";
654
663
  /** What the answers said: the reflexes ranked, every choice read, and the winner with its values. */
655
664
  export interface Reading {
656
665
  /** Sorted by route probability; `try` prints it. */
@@ -877,8 +886,16 @@ export interface Ref {
877
886
  many?: boolean;
878
887
  p?: Prob;
879
888
  }
880
- /** How a segment that matched nothing on its own was settled. */
881
- export type Repair = "narrowed" | "spliced" | "merged";
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
+ }
882
899
  /** One step of the plan: a segment's text and the foundation's decision on it, in the order it is to happen. */
883
900
  export interface Step {
884
901
  /** From 1, as the plan prints it. */
@@ -891,6 +908,8 @@ export interface Step {
891
908
  effect?: Effect;
892
909
  refs?: Ref[];
893
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>;
894
913
  /** The steps this one must follow: an explicit `then`, or a binding. */
895
914
  after?: number[];
896
915
  }
@@ -1100,6 +1119,12 @@ export type Change = {
1100
1119
  } | {
1101
1120
  type: "needs_narrowed";
1102
1121
  removed: Needs;
1122
+ } | {
1123
+ type: "platform_added";
1124
+ platform: Platform;
1125
+ } | {
1126
+ type: "platform_removed";
1127
+ platform: Platform;
1103
1128
  } | {
1104
1129
  type: "required";
1105
1130
  arg: ArgName;
@@ -1260,6 +1285,14 @@ export interface Theft {
1260
1285
  phrase: Utterance;
1261
1286
  owner: LocalName;
1262
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[];
1263
1296
  }
1264
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. */
1265
1298
  export interface Calibration {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-build/evoke",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "You invoke a function; you evoke a reflex. The SDK: load a project of reflexes, decide with Jev, run.",
5
5
  "keywords": [
6
6
  "jev",
@@ -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 model or the
4
- // kernel: what was refused, a permission or a syscall, and the path, so the host names the declaration's key. It
5
- // exits when stdin closes before a body, and during one aborts the body's `signal` first, so its life is bounded
6
- // by its parent's and the body still hears the end. The body's console and stdout go to stderr, as do the frames
7
- // of an error it throws — the frames alone, without the message the host reports, and without the frames inside
8
- // Node itself; an error thrown from a callback ends the body as a rejection does. SIGTERM, the deadline and
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 ? { error: message } : { error: message, refused });
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} ms`, true), 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 frames on stderr, then the
98
- // message and what refused the body, when something did, as the one line.
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
- if (frames) process.stderr.write(`${frames}\n`);
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
- .join("\n");
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) {