@evoke-build/evoke 0.6.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.
@@ -0,0 +1,623 @@
1
+ // A project: the owned files under a root, reflexes handed as code, and who answers — read, verified and compiled
2
+ // once; never fetched, never written. On it: `with`, the same project over a vocabulary of the call's own — a
3
+ // tenant's words, their own plan in milliseconds; `decide`, one input asked, answered, read and gated; `fill`, an
4
+ // ask answered and gated again; `run`, the chosen call's body; `handle`, the whole loop; `steps`, one request
5
+ // read into steps, and `weave`, the steps run stage by stage under the same handlers. In: LoadOptions; inputs;
6
+ // decisions. Out: a Project, decisions, results, plans.
7
+ import { realpathSync, statSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { answered } from "./adapter.js";
10
+ import { facts, scratch, status } from "./contain.js";
11
+ import { bug, call, command, fromCode, misnamed, problem, reply } from "./core.js";
12
+ import { DiagnosticError, FailureError } from "./errors.js";
13
+ import { entry, snapshot, text } from "./files.js";
14
+ import { Refusal, child, inline, program, resolved } from "./runtime.js";
15
+ /** The home project as the CLI writes it on first use: what a root without evoke.toml means. */
16
+ const DEFAULT = { adapter: "jev", reflexes: {}, config: {}, adapters: {} };
17
+ export async function load(options) {
18
+ const { root } = options;
19
+ if (root !== undefined && !directory(root)) {
20
+ throw refused(undefined, `${JSON.stringify(root)} is not a directory`, { type: "rerun" }, "load({ root })");
21
+ }
22
+ const files = root === undefined ? { overlays: {}, vocab: {} } : snapshot(root);
23
+ const project = files.project === undefined ? DEFAULT : call("project", { doc: { file: { type: "project" }, toml: files.project } }, "load()");
24
+ const lock = files.lock === undefined ? undefined : call("lock", { doc: { file: { type: "lock" }, toml: files.lock } }, "load()");
25
+ const adapter = options.adapter ?? (await named(project, root));
26
+ const reflexes = {};
27
+ const dirs = {};
28
+ const bodies = {};
29
+ const shipped = {};
30
+ const local = new Set();
31
+ const given = (options.reflexes ?? {});
32
+ for (const name of Object.keys(given)) {
33
+ const why = misnamed(name, "local");
34
+ if (why !== undefined)
35
+ throw refused(undefined, why, { type: "rerun" }, "load({ reflexes })");
36
+ }
37
+ for (const [name, location] of Object.entries(project.reflexes)) {
38
+ if (given[name] !== undefined) {
39
+ throw refused(name, `${name} is both in evoke.toml and passed to load`, { type: "remove", reflex: name });
40
+ }
41
+ const configured = held(project.config[name] ?? {});
42
+ if (location.type === "local") {
43
+ if (root === undefined)
44
+ throw refused(name, `${name} is a local reflex, but load has no root`, { type: "remove", reflex: name });
45
+ const dir = `${root}/${location.path}`;
46
+ const manifest = text(`${dir}/reflex.toml`);
47
+ if (manifest === undefined) {
48
+ const fix = { type: "remove", reflex: name };
49
+ reflexes[name] = { wording: { err: [{ reflex: name, message: `${location.path}/reflex.toml is missing`, fix }] }, consented: "destructive", configured };
50
+ continue;
51
+ }
52
+ const worded = word(name, { file: { type: "manifest", name }, toml: manifest }, files.overlays[name]);
53
+ reflexes[name] = {
54
+ wording: worded.wording,
55
+ consented: worded.manifest?.effect ?? "destructive",
56
+ ...(worded.manifest?.needs === undefined ? {} : { needs: worded.manifest.needs }),
57
+ configured,
58
+ };
59
+ dirs[name] = dir;
60
+ if (worded.manifest !== undefined)
61
+ shipped[name] = worded.manifest;
62
+ local.add(name);
63
+ continue;
64
+ }
65
+ const locked = lock?.reflexes[name];
66
+ if (locked === undefined) {
67
+ throw refused(name, `${name} is not locked`, { type: "add_ref", reference: call("location", { location }), name });
68
+ }
69
+ const dir = entry(locked.h1);
70
+ if (dir === undefined)
71
+ throw refused(name, `${name} is not in the store`, { type: "sync" });
72
+ const manifest = text(`${dir}/reflex.toml`);
73
+ if (manifest === undefined)
74
+ throw refused(name, `${name} has no reflex.toml in the store`, { type: "sync" });
75
+ const worded = word(name, { file: { type: "manifest", name }, toml: manifest }, files.overlays[name]);
76
+ reflexes[name] = { wording: worded.wording, consented: locked.effect, ...(locked.needs === undefined ? {} : { needs: locked.needs }), configured };
77
+ dirs[name] = dir;
78
+ if (worded.manifest !== undefined)
79
+ shipped[name] = worded.manifest;
80
+ }
81
+ for (const [name, handed] of Object.entries(given)) {
82
+ if (handed === undefined)
83
+ continue;
84
+ const worded = word(name, { file: { type: "manifest", name }, json: { reflex: 1, ...handed.manifest } }, files.overlays[name]);
85
+ // The manifest is the app's own code: its problems are the app's to fix now, not a reflex to leave inactive.
86
+ if ("err" in worded.wording && worded.manifest === undefined)
87
+ throw fromCode(worded.wording.err, `reflex(${name})`);
88
+ reflexes[name] = { wording: worded.wording, consented: worded.manifest?.effect ?? "destructive", configured: {} };
89
+ bodies[name] = handed.body;
90
+ }
91
+ const vocab = {};
92
+ for (const [name, toml] of Object.entries(files.vocab)) {
93
+ vocab[name] = call("vocabulary", { doc: { file: { type: "vocab", name }, toml } }, "load()");
94
+ }
95
+ const installed = { reflexes, vocab, adapter: adapter.id, evoke: call("version", {}) };
96
+ return make({ installed, adapter, dirs, bodies, shipped, local }, "load()");
97
+ }
98
+ /** The adapter evoke.toml names, from its own subpath; a recording is never resolved by name. */
99
+ async function named(project, root) {
100
+ const name = project.adapter;
101
+ const line = (message, adapter) => {
102
+ const form = root === undefined ? `load({ reflexes, adapter: ${adapter} })` : `load({ root, adapter: ${adapter} })`;
103
+ return Promise.reject(new DiagnosticError([{ message, fix: { type: "rerun" }, command: form }]));
104
+ };
105
+ if (root === undefined)
106
+ return line("no adapter: none is named and none was passed", "jev()");
107
+ if (name === "jev" || name === "openjev") {
108
+ const { through } = await import("./systemone.js");
109
+ return through(name, { table: project.adapters[name] });
110
+ }
111
+ if (name === "replay")
112
+ return line(`adapter "replay" names a recording; pass one`, "replay(file)");
113
+ return line(`adapter "${name}" is unknown; pass one`, "adapter");
114
+ }
115
+ /** A manifest read with the overlay named for it: the effective wording, or the problems, which make the reflex
116
+ * inactive rather than refusing the load. */
117
+ function word(name, doc, overlay) {
118
+ const parsed = reply("manifest", { doc });
119
+ if ("bug" in parsed)
120
+ throw bug(parsed.bug);
121
+ if ("err" in parsed)
122
+ return { wording: { err: parsed.err } };
123
+ const manifest = parsed.ok;
124
+ let yours;
125
+ if (overlay !== undefined) {
126
+ const read = reply("overlay", { doc: { file: { type: "overlay", name }, toml: overlay }, of: manifest });
127
+ if ("bug" in read)
128
+ throw bug(read.bug);
129
+ if ("err" in read)
130
+ return { wording: { err: read.err }, manifest };
131
+ yours = read.ok;
132
+ }
133
+ const effective = call("effective", { shipped: manifest, ...(yours === undefined ? {} : { yours }) });
134
+ return { wording: { ok: effective }, manifest };
135
+ }
136
+ /** How each configured key is held: the value, or a variable and whether it is set — never a secret's value. */
137
+ function held(settings) {
138
+ const held = {};
139
+ for (const [key, setting] of Object.entries(settings)) {
140
+ held[key] = setting.type === "plain" ? setting : { type: "env", var: setting.var, set: process.env[setting.var] !== undefined };
141
+ }
142
+ return held;
143
+ }
144
+ function directory(path) {
145
+ try {
146
+ return statSync(path).isDirectory();
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ }
152
+ function refused(reflex, message, fix, invoked = "load()") {
153
+ return new DiagnosticError([{ ...(reflex === undefined ? {} : { reflex }), message, fix, command: command(fix, invoked) }]);
154
+ }
155
+ /** The SDK call as a fix line names it, the input elided past sixty characters. */
156
+ function invocation(input) {
157
+ return `decide(${JSON.stringify(shown(input))})`;
158
+ }
159
+ /** An input with nothing in it never reaches the adapter: refused, with the call to make. */
160
+ function nonEmpty(input, what) {
161
+ if (input.trim() === "")
162
+ throw refused(undefined, "the input is empty", { type: "rerun" }, `${what}("<input>")`);
163
+ }
164
+ /** An input as a fix line shows it: elided past sixty characters. */
165
+ function shown(input) {
166
+ return input.length > 60 ? `${input.slice(0, 59)}…` : input;
167
+ }
168
+ /** What was asked, as one key: the text, the tags, the one reflex. */
169
+ function key(asked) {
170
+ return JSON.stringify([asked.text, asked.tags ?? [], asked.only ?? null]);
171
+ }
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. */
174
+ function askedFor(step, tags) {
175
+ const own = step.repair === "narrowed" || step.repair === "spliced" ? step.reflex : undefined;
176
+ return own === undefined ? { text: step.text, tags } : { text: step.text, only: own };
177
+ }
178
+ /** The project over its ground: the set compiled, every reflex's status read off the plan. The implementation
179
+ * speaks the wire's shapes; the app's R lives on the interface alone. */
180
+ 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);
183
+ if (adapter.plan !== undefined && adapter.plan !== plan.digest) {
184
+ const message = `the recording was made against plan ${adapter.plan}, not ${plan.digest}`;
185
+ throw new DiagnosticError([{ message, fix: { type: "rerun" }, command: "replay(file, { record: jev() })" }]);
186
+ }
187
+ const reflexes = {};
188
+ for (const name of Object.keys(installed.reflexes)) {
189
+ const active = plan.active[name];
190
+ if (active !== undefined) {
191
+ reflexes[name] = { active: true, effect: active.effect, runs: bodies[name] !== undefined ? "inline" : Array.isArray(active.run) ? "argv" : "file" };
192
+ }
193
+ else {
194
+ reflexes[name] = { active: false, problems: (plan.inactive[name] ?? []).map(diagnostic => problem(diagnostic, invoked)) };
195
+ }
196
+ }
197
+ const gate = adapter.gate === undefined ? {} : { gate: adapter.gate };
198
+ const project = {
199
+ reflexes,
200
+ plan: plan.digest,
201
+ with({ vocab }) {
202
+ const replaced = { ...installed.vocab };
203
+ for (const [name, words] of Object.entries(vocab)) {
204
+ const why = misnamed(name, "vocab");
205
+ if (why !== undefined)
206
+ throw new DiagnosticError([{ message: why, fix: { type: "rerun" }, command: "with({ vocab })" }]);
207
+ const read = reply("vocabulary", { doc: { file: { type: "vocab", name }, json: words } });
208
+ if ("bug" in read)
209
+ throw bug(read.bug);
210
+ if ("err" in read)
211
+ throw fromCode(read.err, `with({ vocab: { ${name} } })`);
212
+ replaced[name] = read.ok;
213
+ }
214
+ return make({ ...ground, installed: { ...installed, vocab: replaced } }, "with({ vocab })");
215
+ },
216
+ async decide(input, options = {}) {
217
+ nonEmpty(input, "decide");
218
+ const invoked = invocation(input);
219
+ const request = call("request", { plan, input, tags: options.tags ?? [], ...(options.only === undefined ? {} : { only: options.only }), scope: "full" }, invoked);
220
+ const { raw, trace } = await answered(adapter, request, plan.deadline, options.signal, invoked);
221
+ const reading = call("read", { plan, request, raw }, invoked);
222
+ const decision = call("gate", { plan, reading, ...gate });
223
+ return lined(decision, { input, plan: plan.digest, trace: [trace] });
224
+ },
225
+ fill(decision, given) {
226
+ const asking = own(decision, "fill");
227
+ const invoked = `fill(d, ${JSON.stringify(given)})`;
228
+ const wanted = asking.missing.map(missing => missing.arg);
229
+ const values = {};
230
+ for (const [name, answer] of Object.entries(given)) {
231
+ if (answer === undefined)
232
+ continue;
233
+ const missing = asking.missing.find(missing => missing.arg === name);
234
+ if (missing === undefined) {
235
+ throw refused(undefined, `${name} is not being asked; the ask wants ${wanted.join(", ")}`, { type: "rerun" }, invoked);
236
+ }
237
+ values[name] = typed(missing, String(answer), invoked);
238
+ }
239
+ // The whole decision crosses: the core reads the fields of an Asking and ignores the SDK's own.
240
+ const filled = call("fill", { plan, asking: asking, given: values, ...gate });
241
+ return lined(filled, { input: asking.input, plan: plan.digest, trace: asking.trace });
242
+ },
243
+ async run(decision, options = {}) {
244
+ const chosen = own(decision, "run");
245
+ const outcome = chosen.outcome;
246
+ if (outcome === "confirm" && options.confirmed !== true) {
247
+ throw new TypeError("a confirm decision runs only with { confirmed: true }");
248
+ }
249
+ if (outcome !== "run" && outcome !== "confirm")
250
+ throw new TypeError(`a ${outcome} decision cannot run`);
251
+ const active = plan.active[chosen.reflex];
252
+ if (active === undefined)
253
+ throw refused(chosen.reflex, `${chosen.reflex} is not active`, { type: "rerun" }, "run(d)");
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);
266
+ },
267
+ async handle(input, options = {}) {
268
+ nonEmpty(input, "handle");
269
+ const readied = await ready(await project.decide(input, options), options);
270
+ if (readied.ready)
271
+ return { outcome: "ran", decision: readied.decision, result: await bodied(readied.decision, options.signal) };
272
+ if (readied.status === "refused")
273
+ return { outcome: "abstained", decision: readied.decision };
274
+ return { outcome: readied.status, decision: readied.decision };
275
+ },
276
+ async steps(input, options = {}) {
277
+ nonEmpty(input, "steps");
278
+ return planned(input, options, { decided: [] }, new Map());
279
+ },
280
+ async weave(input, options = {}) {
281
+ nonEmpty(input, "weave");
282
+ const traces = new Map();
283
+ const answers = { decided: [] };
284
+ let woven = await planned(input, options, answers, traces);
285
+ // What the plan asks before anything runs: a step's own question, answered, and the plan made again.
286
+ while (woven.verdict.outcome === "ask") {
287
+ const filled = await askedUpFront(woven, answers, options, traces);
288
+ if (filled !== "filled")
289
+ return { plan: woven, status: filled, steps: [] };
290
+ const again = await planned(input, options, answers, traces);
291
+ // A plan that asks the same again could not take the answer: unanswered, never a loop.
292
+ if (JSON.stringify(again.verdict) === JSON.stringify(woven.verdict))
293
+ return { plan: again, status: "unanswered", steps: [] };
294
+ woven = again;
295
+ }
296
+ if (woven.verdict.outcome === "refuse")
297
+ return { plan: woven, status: "refused", steps: [] };
298
+ if (woven.verdict.outcome === "confirm") {
299
+ if (options.proceed === undefined)
300
+ return { plan: woven, status: "unanswered", steps: [] };
301
+ if (!(await options.proceed(woven)))
302
+ return { plan: woven, status: "declined", steps: [] };
303
+ }
304
+ return executed(woven, options, traces);
305
+ },
306
+ };
307
+ /** The plan over the answers gathered so far: the adapter asked and texts decided until it stands. */
308
+ async function planned(input, options, answers, traces) {
309
+ const invoked = `steps(${JSON.stringify(shown(input))})`;
310
+ for (;;) {
311
+ const planning = call("weave.plan", { plan, input, tags: options.tags ?? [], answers }, invoked);
312
+ if (planning.type === "done")
313
+ return planning.weave;
314
+ const { need } = planning;
315
+ if (need.type === "decide") {
316
+ // Side by side: each text is its own adapter call.
317
+ answers.decided.push(...(await Promise.all(need.asked.map(async (asked) => [asked, await decided(asked, options, traces)]))));
318
+ continue;
319
+ }
320
+ const { raw } = await answered(adapter, need.request, plan.deadline, options.signal, invoked);
321
+ if (need.type === "judge")
322
+ answers.judged = raw;
323
+ else
324
+ answers.referred = raw;
325
+ }
326
+ }
327
+ /** One text decided as the plan asks — over the tags, or one reflex alone — its trace kept by what was asked. */
328
+ async function decided(asked, options, traces) {
329
+ const decision = await project.decide(asked.text, {
330
+ ...(asked.only === undefined ? { tags: asked.tags ?? [] } : { only: asked.only }),
331
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
332
+ });
333
+ traces.set(key(asked), decision.trace);
334
+ // The whole decision crosses: the core reads its own fields and ignores the SDK's.
335
+ return decision;
336
+ }
337
+ /** The plan's own questions before anything runs. A step's required argument no binding covers is asked as
338
+ * `handle` asks one — the ask narrowed to what nothing binds — and the step's decision replaced for the plan
339
+ * to stand again. Several fields, or one record of several, no handler can answer: unanswered. */
340
+ async function askedUpFront(woven, answers, options, traces) {
341
+ const because = woven.verdict.because ?? [];
342
+ if (options.ask === undefined || because.some(b => b.type === "several" || b.type === "one_of_many"))
343
+ return "unanswered";
344
+ for (const n of new Set(because.flatMap(b => (b.type === "needs" ? [b.step] : [])))) {
345
+ const step = woven.steps[n - 1];
346
+ if (step === undefined || step.decision.outcome !== "ask")
347
+ continue;
348
+ const bound = new Set((woven.binds ?? []).filter(b => b.to === n).map(b => b.arg));
349
+ const asked = askedFor(step, options.tags ?? []);
350
+ let decision = lined(step.decision, { input: step.text, plan: plan.digest, trace: traces.get(key(asked)) ?? [] });
351
+ let refusals = 0;
352
+ while (decision.outcome === "ask") {
353
+ const unbound = decision.missing.filter(m => !bound.has(m.arg));
354
+ if (unbound.length === 0)
355
+ break;
356
+ const narrowed = { ...decision, missing: unbound };
357
+ const given = await options.ask(narrowed, { step: n, round: 0 });
358
+ if (given === undefined)
359
+ return "declined";
360
+ let filled;
361
+ try {
362
+ filled = project.fill(narrowed, given);
363
+ }
364
+ catch (error) {
365
+ if (!(error instanceof DiagnosticError))
366
+ throw error;
367
+ // An answer that does not read is asked again once; twice is a decline.
368
+ if (refusals++ > 0)
369
+ return "declined";
370
+ continue;
371
+ }
372
+ if (filled.outcome === "ask" && same(filled.missing, decision.missing))
373
+ return "declined";
374
+ decision = filled;
375
+ }
376
+ for (const entry of answers.decided)
377
+ if (key(entry[0]) === key(asked))
378
+ entry[1] = decision;
379
+ }
380
+ return "filled";
381
+ }
382
+ /** The plan run: stage by stage, a stage's rounds each taken to the point of running in the words' order, then
383
+ * their bodies together; a step decided again with its bound values in its words when the run asks for it. */
384
+ async function executed(woven, options, traces) {
385
+ const invoked = `weave(${JSON.stringify(shown(woven.input))})`;
386
+ const progress = { decided: [], handled: [] };
387
+ const rounds = new Map();
388
+ const record = (handling, decision, became) => {
389
+ const { status, why, result } = became;
390
+ const said = { ...(why === undefined ? {} : { why }), ...(result === undefined ? {} : { result: result }) };
391
+ progress.handled.push({ step: handling.step, round: handling.round, status, ...said });
392
+ const list = rounds.get(handling.step) ?? [];
393
+ list.push({ round: handling.round, input: handling.input, decision, status, ...said });
394
+ rounds.set(handling.step, list);
395
+ };
396
+ for (;;) {
397
+ const running = call("weave.execute", { plan, ...gate, weave: woven, progress }, invoked);
398
+ if (running.type === "done") {
399
+ const { executed } = running;
400
+ return {
401
+ plan: woven,
402
+ status: executed.worst,
403
+ steps: executed.steps.map(step => ({
404
+ step: step.step,
405
+ status: step.status,
406
+ ...(step.why === undefined ? {} : { why: step.why }),
407
+ bound: step.bound ?? [],
408
+ rounds: rounds.get(step.step) ?? [],
409
+ })),
410
+ };
411
+ }
412
+ const { todo } = running;
413
+ if (todo.type === "decide") {
414
+ progress.decided.push([todo.asked, await decided(todo.asked, options, traces)]);
415
+ continue;
416
+ }
417
+ const bodies = [];
418
+ for (const handling of todo.handling) {
419
+ const step = woven.steps[handling.step - 1];
420
+ // A round decided again with its values in its words has its own trace; any other round has its step's.
421
+ const own = step?.reflex === undefined || !handling.bound?.length ? undefined : traces.get(key({ text: handling.input, only: step.reflex }));
422
+ const trace = own ?? (step === undefined ? undefined : traces.get(key(askedFor(step, options.tags ?? [])))) ?? [];
423
+ const decision = lined(handling.decision, { input: handling.input, plan: plan.digest, trace });
424
+ const readied = await ready(decision, told(options, { step: handling.step, round: handling.round }));
425
+ if (!readied.ready) {
426
+ record(handling, readied.decision, { status: readied.status, why: readied.why });
427
+ continue;
428
+ }
429
+ bodies.push(ran(readied.decision, options.signal).then(became => record(handling, readied.decision, became)));
430
+ }
431
+ await Promise.all(bodies);
432
+ }
433
+ }
434
+ /** The foundation's loop up to the run, as `handle` takes a decision: ask and fill until nothing is missing,
435
+ * then confirm. What stands ready to run, or where it stopped and why. */
436
+ async function ready(start, options) {
437
+ let decision = start;
438
+ let refusals = 0;
439
+ for (;;) {
440
+ switch (decision.outcome) {
441
+ case "abstain":
442
+ return { ready: false, status: "refused", decision, why: { type: "no_reflex" } };
443
+ case "ask": {
444
+ const why = { type: "said", message: decision.missing.map(m => m.arg).join(", ") };
445
+ if (options.ask === undefined)
446
+ return { ready: false, status: "unanswered", decision, why };
447
+ const given = await options.ask(decision);
448
+ if (given === undefined)
449
+ return { ready: false, status: "declined", decision, why };
450
+ let filled;
451
+ try {
452
+ filled = project.fill(decision, given);
453
+ }
454
+ catch (error) {
455
+ if (!(error instanceof DiagnosticError))
456
+ throw error;
457
+ // An answer that does not read is asked again once; twice is a decline.
458
+ if (refusals++ > 0)
459
+ return { ready: false, status: "declined", decision, why };
460
+ continue;
461
+ }
462
+ // An answer that leaves the ask exactly as it was is a decline, so a handler that never answers ends.
463
+ if (filled.outcome === "ask" && same(filled.missing, decision.missing))
464
+ return { ready: false, status: "declined", decision: filled, why };
465
+ decision = filled;
466
+ continue;
467
+ }
468
+ case "confirm": {
469
+ const why = { type: "said", message: decision.prompt.own };
470
+ if (options.confirm === undefined)
471
+ return { ready: false, status: "unanswered", decision, why };
472
+ if (!(await options.confirm(decision)))
473
+ return { ready: false, status: "declined", decision, why };
474
+ return { ready: true, decision };
475
+ }
476
+ case "run":
477
+ return { ready: true, decision };
478
+ }
479
+ }
480
+ }
481
+ /** `weave`'s handlers as `ready` takes them: each told which step and round asks. */
482
+ function told(options, turn) {
483
+ const { confirm, ask } = options;
484
+ return {
485
+ ...(confirm === undefined ? {} : { confirm: (decision) => confirm(decision, turn) }),
486
+ ...(ask === undefined ? {} : { ask: (decision) => ask(decision, turn) }),
487
+ };
488
+ }
489
+ /** A decision ready to run, run: a confirm once confirmed. */
490
+ function bodied(decision, signal) {
491
+ const options = signal === undefined ? {} : { signal };
492
+ return decision.outcome === "confirm" ? project.run(decision, { ...options, confirmed: true }) : project.run(decision, options);
493
+ }
494
+ /** One body run for a weave: what it returned, or its failure as the step's own outcome, never the weave's. */
495
+ async function ran(decision, signal) {
496
+ try {
497
+ return { status: "ran", result: await bodied(decision, signal) };
498
+ }
499
+ catch (error) {
500
+ if (error instanceof FailureError) {
501
+ return { status: "failed", why: { type: "said", message: error.why === undefined ? error.what : `${error.what}: ${error.why}` } };
502
+ }
503
+ throw error;
504
+ }
505
+ }
506
+ /** A file or an argv body under its declaration: the policy resolved with the call's values, the machine's facts
507
+ * gathered — a declared path or program it lacks is the failure, before anything runs — then the loader or the
508
+ * program in the body's directory with a private temporary folder, the layers around it; a refusal past the
509
+ * declaration names the path, the key and the fix. */
510
+ async function contained(what, chosen, active, dir, envelope, signal) {
511
+ const reflex = chosen.reflex;
512
+ const wire = chosen;
513
+ const called = { reflex, args: chosen.args, call: chosen.call };
514
+ const config = resolved(what, envelope.config);
515
+ const home = homedir();
516
+ const policy = call("needs.resolve", { needs: active.needs ?? {}, call: called, active, config, home }, "run(d)");
517
+ const argv = typeof active.run === "string" ? undefined : call("argv", { chosen: wire, active, home }, "run(d)");
518
+ const body = realpathSync(dir);
519
+ const origin = () => {
520
+ if (!local.has(reflex))
521
+ return { type: "fetched" };
522
+ const file = { type: "manifest", name: reflex };
523
+ const toml = text(`${dir}/reflex.toml`) ?? "";
524
+ return { type: "local", at: call("needs.declared_at", { doc: { file, toml } }) };
525
+ };
526
+ const named = (diagnostic) => new FailureError(what, diagnostic.message, diagnostic.fix, command(diagnostic.fix, "run(d)"));
527
+ const kind = argv === undefined ? "file" : "argv";
528
+ const tmp = scratch();
529
+ try {
530
+ const gathered = facts(policy, kind, body, tmp.path);
531
+ if ("place" in gathered || "program" in gathered) {
532
+ const lacking = "place" in gathered ? { type: "place", place: gathered.place, key: gathered.key } : { type: "program", program: gathered.program };
533
+ throw named(call("needs.lacking", { lacking, reflex, active, origin: origin(), home }));
534
+ }
535
+ const layers = { policy, facts: gathered };
536
+ let result;
537
+ try {
538
+ result =
539
+ argv === undefined
540
+ ? await child(what, body, { ...envelope, run: active.run }, layers, signal)
541
+ : await program(what, argv, envelope, layers, signal);
542
+ }
543
+ catch (error) {
544
+ if (!(error instanceof Refusal))
545
+ throw error;
546
+ const at = origin();
547
+ let upstream;
548
+ const declared = shipped[reflex]?.needs;
549
+ if (at.type === "fetched" && declared !== undefined) {
550
+ const answer = reply("needs.resolve", { needs: declared, call: called, active, config, home });
551
+ if ("ok" in answer)
552
+ upstream = answer.ok;
553
+ }
554
+ const diagnostic = call("needs.refusal", { policy, ...(upstream === undefined ? {} : { upstream }), reflex, origin: at, refused: error.refused, home });
555
+ if (diagnostic === null)
556
+ throw new FailureError(what, error.message, { type: "rerun" }, command({ type: "rerun" }, "run(d)"));
557
+ throw named(diagnostic);
558
+ }
559
+ return { ...result, contained: status(kind) };
560
+ }
561
+ finally {
562
+ tmp.remove();
563
+ }
564
+ }
565
+ /** A decision made under this plan; another plan's is misuse — the wrong tenant's project, or words that changed. */
566
+ function own(decision, what) {
567
+ if (decision.plan !== plan.digest) {
568
+ throw new TypeError(`the decision was made under another plan; ${what} it on the project that decided it`);
569
+ }
570
+ return decision;
571
+ }
572
+ return project;
573
+ }
574
+ /** Whether two asks want the same things for the same reasons. */
575
+ function same(after, before) {
576
+ return JSON.stringify(after) === JSON.stringify(before);
577
+ }
578
+ /** The wire decision with the SDK's fields: input, plan, trace, and the plain values of a chosen or asking call. */
579
+ function lined(decision, line) {
580
+ if (decision.outcome === "abstain")
581
+ return { ...decision, ...line };
582
+ return { ...decision, values: call("values", { args: decision.args }), ...line };
583
+ }
584
+ /** What a person answered, as the value the ask offered: a key among the options, a word among the words, a pick
585
+ * through its recognizer; anything else is refused naming the argument and what it may be. */
586
+ function typed(missing, answer, invoked) {
587
+ const { choices } = missing;
588
+ const text = answer.trim();
589
+ const fix = { type: "rerun" };
590
+ switch (choices.type) {
591
+ case "options": {
592
+ if (Object.hasOwn(choices.options, text))
593
+ return { type: "option", key: text };
594
+ throw refused(undefined, `${missing.arg}: ${JSON.stringify(answer)} is not one of ${Object.keys(choices.options).join(", ")}`, fix, invoked);
595
+ }
596
+ case "vocab": {
597
+ if (Object.hasOwn(choices.words, text))
598
+ return { type: "word", word: text };
599
+ throw refused(undefined, `${missing.arg}: ${JSON.stringify(answer)} is not one of ${Object.keys(choices.words).join(", ")}`, fix, invoked);
600
+ }
601
+ case "pick": {
602
+ const picked = call("picked", { text, recognizer: choices.pick });
603
+ if (picked !== null)
604
+ return picked;
605
+ throw refused(undefined, `${missing.arg}: ${JSON.stringify(answer)} is not ${wants(choices.pick)}`, fix, invoked);
606
+ }
607
+ }
608
+ }
609
+ /** What a recognizer reads, in the core's words. */
610
+ function wants(recognizer) {
611
+ switch (recognizer) {
612
+ case "number":
613
+ return "a number";
614
+ case "duration":
615
+ return "a duration";
616
+ case "email":
617
+ return "an email address";
618
+ case "url":
619
+ return "a URL";
620
+ case "quoted":
621
+ return "text";
622
+ }
623
+ }