jevstrudel.git / website / src / jev / jevCore.mjs
1// `jev({...})` — TypeSafe's API as Strudel patterns, in the shape of its
2// SDK (`@typesafe-ai/sdk`: `client.systemOne({ state, questions })`, with
3// questions built by `choice`, `score` and `noul`). A song declares its
4// Jev once, at the top, and composes the answers in code:
5//
6//   const djJev = jev({                          // TypeSafe's request: state and questions
7//     state: { title: 'LIGHTNING IN A BOTTLE', key: 'E minor' },
8//     questions: {
9//       section: choice('Pick the next section', { intro: '…', riff: '…', …, outro: '…' }),
10//       move: choice('Pick which parts play it', { asWritten: '…', noDrums: '…' }),
11//       energy: score('How intense should it be?', ['held back', 'steady', 'driving', 'all out']),
12//     },
13//   })
14//   const { section, move, energy } = djJev.every(8, { fallback: { energy: 3 } })   // Strudel: when to ask
15//   …
16//   const form = walk(section, { max: 16, sections: { intro: { at: 0, next: ['riff'] }, … } })
17//   form.arrange(move.choice.pick({ asWritten: song, noDrums: … })).velocity(energy.score.div(3).range(.55, 1))
18//
19// `jev({ state, questions })` is exactly the SDK's request; nothing about
20// time is Jev's. `djJev.every(cycles, { fallback })` is Strudel's side: it
21// asks every `cycles` cycles and returns the answers, mirroring the SDK's
22// `response.answers`, with a pattern (one value per `cycles`) for each field:
23// `.choice`, `.confidence` and `.probabilities` for a choice; `.score`,
24// `.confidence` and `.probabilities` for a score (on the rubric's own scale,
25// 0 to levels-1); `.noul` for a noul.
26//
27// `fallback` (to `.every`) is what plays when Jev has not answered, or answered with
28// something unusable: a choice falls back to its first option unless named;
29// a score and a noul must name theirs, since nothing about a ladder or a
30// yes/no says which end is "as written".
31//
32// `walk(answer, { max, start, sections })` is not Jev: it is the song's
33// form as rules in code, as TypeSafe's docs ask ("keep control flow and
34// deterministic rules in code"). Each section is one of a choice's options,
35// with `at` (the cycle it starts at as written) and `next` (the sections
36// allowed to follow it; empty ends the song), and optionally `times` (the
37// most times it plays in one performance). It narrows that choice, per
38// request, to the options allowed next (or skips it when only one is), makes
39// the written order its fallback, forces an ending at `max` sections (16 by
40// default), ends the song after an ending section, and `form.arrange(pat)`
41// plays each section's own cycles of `pat`.
42//
43// `times` counts every play of the section, not a run of it: a loop is
44// usually a cycle of different sections (groove a → b → c → a), where no one
45// section ever repeats back to back, so only a total bounds it: a cycle
46// through a section with `times: 2` goes round at most twice. A section that
47// has played its `times` is no longer offered, nor is any section whose only
48// ways to an ending now run through one; there is always a way on (see
49// `stepsToEnd`), and the written order, which plays each section once, is
50// never capped.
51//
52// A jev() asked after a form (`after: { section }`) arranges each section
53// the form picks; the form's own history rows carry what it settled for
54// each past section as `arrangement` (its questions, less the ones it
55// `forget`s and parts absent there), so the form's Jev can hear what the
56// arrangement built and released before it picks the next section.
57//
58// Jev only ever selects among what the song wrote down, and anything
59// unusable plays the fallback and says why in the console. All of the text
60// goes in 'single quotes': "double quotes" and backticks are mini-notation.
61//
62// One request per section, as TypeSafe advises ("ask independent questions
63// together, then compose their answers in code"): every question for
64// section n+1 goes to Jev at once, as soon as the song first queries
65// section n+1's answers: halfway through section n at the latest, and
66// earlier when a song reads ahead (a transition's `.early(1)` asks a bar into
67// section n, which gives a chained `after:` request its time but leaves out
68// 🔥/😴 from the rest of section n), with the song's `state`, the history of answers, the
69// reactions, and, with a form, where the song is in it. A section that
70// begins before its answer lands plays its fallbacks to its end. "Begins"
71// is playing time: the earliest cycle the song queried in one scheduler
72// tick, so a read ahead (`.early(1)`) does not count as the section
73// beginning. A question read ahead while its answer is out has already
74// played its fallback, though: it keeps it for that section (and says so),
75// so a transition never switches halfway through its bar.
76//
77// `allow: { name: (decided) => [options] }` (to `.every`) narrows a choice
78// per request, the way a form narrows its sections: `decided` holds the
79// `after:` answers (`{ section: 'drop1' }`) and, when one of them walks a
80// form, `playingNow`, the section before it. Rules like "no riser out of a
81// build" are then code, not a sentence Jev may ignore. The fallback must
82// always be allowed; a single allowed option is played without asking.
83//
84// Listeners: `listeners()` (to createJev) is every listener's 🔥/😴 tally
85// for the song playing, { [section]: { fire, sleep } } (reactions.mjs), or
86// null. Each request carries it as `listenersSoFar`, cut to the sections
87// that matter to the answer: the last few played and those that may come
88// next (or the one being entered, for a jev() asked after a form's), at
89// most LISTENERS_MAX, and only names the song's form has (the tally is
90// anyone's writing). A song without a form keys sections by number.
91//
92// Replay: `setReplay(performance)` makes every jev() declared after it
93// play a recorded performance (performance.mjs) instead of asking Jev: each
94// segment takes the recorded answers of its jev() (by declaration order),
95// with no request at all. The rules still apply: a recorded answer the
96// song's form or `allow` no longer permits there, or that is no longer an
97// option, plays the fallback and says so, and a segment the recording does
98// not have plays the fallbacks. So a song whose code changed since plays
99// the performance where it still can. `setReplay(null)` goes back to Jev.
100//
101// Decisions can also come from outside (`followDecisions`): a listening
102// party's guests (party.mjs) play the decisions the host's page settled,
103// sent to them as they are made (`watchDecisions`), and never ask Jev. Each
104// decision is checked against the song before it plays, like an answer; one
105// that arrives after its section began is a late answer.
106import { Fraction, Hap, Pattern, TimeSpan, logger } from '@strudel/core';
107import { boothStore } from './boothStore.mjs';
108
109export const JEV_MODEL = 'jev-1.13.0';
110
111const isText = (x) => typeof x === 'string';
112const isObject = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
113
114function text(where, value) {
115  // The transpiler turns double-quoted and backtick strings into mini-notation.
116  if (value instanceof Pattern) {
117    throw new Error(`jev: write ${where} in 'single quotes'; "double quotes" and backticks are mini-notation`);
118  }
119  if (!isText(value)) throw new Error(`jev: ${where} must be text`);
120  return value;
121}
122
123// The section that follows `name` in the song as written, if any.
124function writtenNext(sections, name, every) {
125  const at = sections[name].at + every;
126  return Object.keys(sections).find((n) => sections[n].at === at);
127}
128
129// How many sections must still play after each one to reach an ending,
130// through only the sections that may still play (`open`); Infinity where
131// none can. A shortest way is a simple path, so it plays each section on it
132// at most once: a section open now (one play left, at least) can be on it.
133// That is why a request never runs out of sections: the section chosen at
134// segment n could reach an ending by the cap through sections open then;
135// playing it closes at most itself, which its own shortest way never
136// revisits, so the next section on that way is still open, one step
137// nearer, and offered at n + 1.
138function stepsToEnd(sections, open = () => true) {
139  const all = Object.keys(sections);
140  const d = Object.fromEntries(all.map((n) => [n, open(n) && !sections[n].next.length ? 0 : Infinity]));
141  for (let changed = true; changed; ) {
142    changed = false;
143    for (const n of all) {
144      if (!open(n)) continue;
145      const x = 1 + Math.min(Infinity, ...sections[n].next.map((m) => d[m]));
146      if (x < d[n]) [d[n], changed] = [x, true];
147    }
148  }
149  return d;
150}
151
152// The SDK's question builders, same signatures, same objects on the wire.
153// Their jsdoc (and jev's, below) is the reference tab's and the MCPs'
154// api_reference's (package.json's jsdoc-json reads this file); its examples
155// are evaluated and played in reference.test.mjs.
156
157/**
158 * A question for `jev()`: Jev picks one of the song's options. Each option is a name the code uses and a sentence Jev reads; Jev can only ever answer one of the names, so it cannot make anything up. TypeSafe's `Choice`, with the SDK's signature.
159 *
160 * Its answer, from `.every(...)`, has three patterns with one value per section: `.choice`, the option's name (compose it with Strudel's `pick`); `.confidence`, 0 to 1, how peaked Jev's probabilities are; and `.probabilities`, option name → probability. Without a usable answer it plays its fallback, the first option unless `.every`'s `fallback` names another.
161 *
162 * All of the text goes in 'single quotes': "double quotes" and backticks are mini-notation, and jev() refuses them.
163 * @name choice
164 * @tags jev
165 * @param {string} instructions the question, in 'single quotes'
166 * @param {Object} criteria at least two options, `{ name: 'what it is, for Jev' }`
167 * @returns {Object} the question, for `jev({ questions })`
168 * @example
169 * setcps(.5)
170 * const djJev = jev({
171 *   state: { title: 'TWO MOODS', what: 'a sawtooth chord over a kick' },
172 *   questions: {
173 *     mood: choice('Pick the chord for the next four bars', {
174 *       dark: 'A minor, low and filtered',
175 *       bright: 'C major, open',
176 *     }),
177 *   },
178 * })
179 * const dj = djJev.every(4, { segments: 8 })
180 * stack(
181 *   note(dj.mood.choice.pick({ dark: "a2,c3,e3", bright: "c3,e3,g3" })).s('sawtooth').lpf(1200),
182 *   s("bd*4"),
183 * )
184 */
185export function choice(instructions, criteria) {
186  return { type: 'choice', instructions, criteria };
187}
188
189/**
190 * A question for `jev()`: Jev rates the section on a ladder of 2 to 10 levels the song writes down, low to high. TypeSafe's `Score`, with the SDK's signature.
191 *
192 * Its answer, from `.every(...)`, has three patterns with one value per section: `.score`, from 0 to levels−1 and fractional (Jev's weighted mean over the levels, so `.round()` it for a whole step); `.confidence`, 0 to 1; and `.probabilities`, keyed by level index ('0', '1', …). A score has no natural "as written" end, so `.every` must name its fallback: `fallback: { name: level }`.
193 * @name score
194 * @tags jev
195 * @param {string} instructions the question, in 'single quotes'
196 * @param {Array} criteria 2 to 10 levels, low to high, each a 'label: what it means'
197 * @returns {Object} the question, for `jev({ questions })`
198 * @example
199 * setcps(.5)
200 * const djJev = jev({
201 *   state: { title: 'RAMP', what: 'a kick and a saw bass' },
202 *   questions: {
203 *     energy: score('How intense should the next four bars be?', [
204 *       'held back: half velocity',
205 *       'steady: most of the way',
206 *       'all out: full velocity',
207 *     ]),
208 *   },
209 * })
210 * const dj = djJev.every(4, { fallback: { energy: 2 }, segments: 8 })
211 * stack(s("bd*4"), note("<a1 c2 f1 g1>").s('sawtooth').lpf(600))
212 *   .velocity(dj.energy.score.div(2).range(.5, 1))
213 */
214export function score(instructions, criteria) {
215  return { type: 'score', instructions, criteria };
216}
217
218/**
219 * A question for `jev()`: how likely a yes/no is, as a calibrated probability. TypeSafe's `Noul`, with the SDK's signature; `criteria` says what true and false mean, `{ true: '…', false: '…' }`, either optional.
220 *
221 * Its answer, from `.every(...)`, is one pattern with one value per section: `.noul`, the probability of true, 0 to 1. A noul has no `.confidence`: the probability is the certainty. `.every` must name its fallback: `fallback: { name: 0 }` (or 1, or anything between).
222 * @name noul
223 * @tags jev
224 * @param {string} instructions the question, in 'single quotes'
225 * @param {Object} criteria optional, `{ true: '…', false: '…' }`
226 * @returns {Object} the question, for `jev({ questions })`
227 * @example
228 * setcps(.5)
229 * const djJev = jev({
230 *   state: { title: 'BREAKDOWN', what: 'drums under a pad; the drums can drop out for a section' },
231 *   questions: {
232 *     breakdown: noul('Should the next four bars drop the drums for a breakdown?', {
233 *       true: 'the drums stop, the pad alone',
234 *       false: 'the drums keep going',
235 *     }),
236 *   },
237 * })
238 * const dj = djJev.every(4, { fallback: { breakdown: 0 }, segments: 8 })
239 * stack(
240 *   note("<c3,e3,g3 a2,c3,e3>").s('sawtooth').lpf(900),
241 *   s("bd*4, ~ sd").mask(dj.breakdown.noul.lt(.5)),
242 * )
243 */
244export function noul(instructions = null, criteria = null) {
245  return { type: 'noul', instructions, ...(criteria ? { criteria } : {}) };
246}
247
248// A fallback answer, shaped like Jev's.
249const fallbackAnswer = (q, value) =>
250  q.type === 'choice'
251    ? { type: 'choice', choice: value, confidence: 0, probabilities: {}, fallback: true }
252    : q.type === 'score'
253      ? { type: 'score', score: value, confidence: 0, probabilities: {}, fallback: true }
254      : { type: 'noul', noul: value, fallback: true };
255
256// What a question's answer says, in words, for the history Jev is sent.
257// `brief`: a score's level by its name alone, the words before its colon
258// ('full: as written' is 'full'), for another jev()'s history, which does
259// not carry this one's questions and would otherwise repeat every label.
260function describe(q, a, { brief = false } = {}) {
261  if (q.type === 'choice') return a.choice;
262  if (q.type === 'score') {
263    const label = q.criteria[Math.round(a.score)];
264    return `${Math.round(a.score * 10) / 10} (${brief && isText(label) ? label.split(':')[0] : label})`;
265  }
266  return Math.round(a.noul * 100) / 100;
267}
268
269// How many sections `listenersSoFar` names at most, and how many of them
270// are the ones just played.
271const LISTENERS_MAX = 8;
272const LISTENERS_BACK = 4;
273
274// The performance jev()s declared from now on replay (see the header).
275let replaying = null;
276export function setReplay(performance) {
277  replaying = performance ? { jevs: performance.jevs ?? [], changed: Boolean(performance.changed) } : null;
278}
279
280const cycleText = (c) => (Number.isFinite(c) ? (Math.round(c * 10) / 10).toFixed(1) : 'none yet');
281
282// Ask every jev() of the song about to start (the evaluation's, from the
283// booth) about its opening section, and wait until they answer or `ms`
284// passes: the scheduler awaits it before its first tick (useReplContext's
285// beforeStart), so the opening is Jev's call rather than always the
286// fallbacks. An answer later than `ms` plays from the next cycle.
287export async function askOpening(ms) {
288  const asked = boothStore.starting().map((view) => view.askOpening?.());
289  if (!asked.length) return;
290  let timer;
291  await Promise.race([Promise.all(asked), new Promise((r) => (timer = setTimeout(r, ms)))]);
292  clearTimeout(timer);
293}
294
295// The parts of the song about to start, as written: which orbit sounds in
296// which cycles (preload.mjs reads it off the song queried asWritten). A
297// question named for a part's orbit (`part('drums', …)` plays on
298// `.orbit('drums')`) is not asked about a section where that part has no
299// notes as written; songs are written so, so no song names them by hand.
300// They are attached to the song's own jev()s (the evaluation's, from the
301// booth), so another song evaluated later never inherits them.
302export function setWrittenParts(haps) {
303  const at = new Map();
304  const orbits = new Set();
305  for (const hap of haps) {
306    const orbit = hap.value?.orbit;
307    if (typeof orbit !== 'string' || /^\d+$/.test(orbit) || !hap.whole) continue;
308    orbits.add(orbit);
309    const from = Math.floor(hap.whole.begin.valueOf());
310    const to = Math.ceil(hap.whole.end.valueOf());
311    for (let c = from; c < Math.max(to, from + 1); c++) {
312      if (!at.has(c)) at.set(c, new Set());
313      at.get(c).add(orbit);
314    }
315  }
316  const parts = {
317    orbits,
318    playsIn: (orbit, [from, to]) => {
319      for (let c = from; c < to; c++) if (at.get(c)?.has(orbit)) return true;
320      return false;
321    },
322  };
323  for (const view of boothStore.starting()) view.writtenParts = parts;
324}
325
326// ── decisions from outside ──────────────────────
327// A performance's decisions, shared: a listening party's host sends each one
328// its jev()s settle (watchDecisions), and its guests' jev()s play them
329// instead of asking (followDecisions). The same two ends fit a performance
330// recorded and replayed later: keep what watchDecisions hands out, and feed
331// it back through the receivers followDecisions returns.
332//
333// The views are the song about to start (or playing), in the order its
334// jev()s are declared, which is the same for the same code: a view's index
335// is its name on the wire.
336
337// What of a decision leaves the page: the values and how they came to be,
338// not what Jev was told (`state`: the history and measurements, large and
339// the sender's own). `last` is the segment the song ends after, once known.
340export function shareDecision(decision, last) {
341  const { cycles, status, values, from, planned, problems, reason, ms } = decision;
342  return {
343    cycles,
344    status,
345    values,
346    ...(from !== undefined ? { from, planned } : {}),
347    ...(problems?.length ? { problems } : {}),
348    ...(reason !== undefined ? { reason } : {}),
349    ...(ms !== undefined ? { ms } : {}),
350    last,
351  };
352}
353
354// Calls fn(view, segment, decision) (decision as shareDecision makes it) for
355// every decision the starting song's jev()s settle from now on. Returns the
356// ones already settled, in the same shape, and how to stop.
357export function watchDecisions(fn) {
358  const settled = [];
359  const watching = boothStore.starting().map((view, i) => {
360    for (const d of view.decisions) {
361      if (d && d.status !== 'asking' && d.status !== 'opening') {
362        settled.push({ view: i, segment: d.segment, decision: shareDecision(d, view.last) });
363      }
364    }
365    const watcher = (segment, d) => fn(i, segment, shareDecision(d, view.last));
366    view.watchers.add(watcher);
367    return [view, watcher];
368  });
369  return { settled, stop: () => watching.forEach(([view, w]) => view.watchers.delete(w)) };
370}
371
372// Puts the starting song's jev()s in follow mode: they ask Jev nothing and
373// play what they receive. Returns a receiver per view, in declaration
374// order: `receive(segment, decision) → taken`. Feed it what is already
375// known before the song starts; askOpening then waits for the opening's.
376export function followDecisions() {
377  return boothStore.starting().map((view) => view.follow());
378}
379
380// The listeners' reactions to a section, as totals counted elsewhere (a
381// party's room): every jev() of the playing song hears them.
382export function setReactions(segment, { fire = 0, sleep = 0 }) {
383  for (const view of boothStore.starting()) {
384    view.reactions[segment] = { fire, sleep };
385    boothStore.changed(view);
386  }
387}
388
389// asWritten(fn): queries made inside fn see every jev() as the song was
390// written (each section its written successor, playing its fallbacks) and
391// change nothing: no request goes out, the playhead does not move, nothing
392// is locked. For looking ahead at a whole song, e.g. to load its samples
393// before it plays (preload.mjs); a normal query of a section that has not
394// begun asks Jev about it.
395let writtenDepth = 0;
396export function asWritten(fn) {
397  writtenDepth++;
398  try {
399    return fn();
400  } finally {
401    writtenDepth--;
402  }
403}
404
405// While walk()'s arrange() queries a section's own cycles, the time it
406// shifted them by: an answer composed inside an arrangement (a `.pick`
407// inside `form.arrange(…)`) is queried at the section's written position,
408// and adds this back to find the section actually playing. Queries are
409// synchronous, so a stack is exact.
410const offsets = [];
411const offsetNow = () => offsets.reduce((sum, x) => sum + x, 0);
412const shifted = (pat, shift) =>
413  new Pattern((st) => {
414    offsets.push(shift);
415    try {
416      return pat.late(shift).query(st);
417    } finally {
418      offsets.pop();
419    }
420  });
421
422// Each jev() keeps its internals here, keyed by its answers, so walk() can
423// attach a form to the question an answer came from.
424const owners = new WeakMap();
425
426// Sampling a choice at a temperature: probabilities raised to 1/t and
427// renormalised (t = 1 samples Jev's own distribution; t → 0 is its top
428// pick). `random` is Math.random in the page, a stub in tests.
429function sampleChoice(answer, t, random) {
430  const entries = Object.entries(answer.probabilities ?? {}).filter(([, p]) => p > 0);
431  if (!entries.length || !(t > 0)) return answer;
432  const weights = entries.map(([k, p]) => [k, p ** (1 / t)]);
433  const total = weights.reduce((sum, [, w]) => sum + w, 0);
434  let r = random() * total;
435  for (const [k, w] of weights) {
436    r -= w;
437    if (r <= 0) return k === answer.choice ? answer : { ...answer, choice: k, top: answer.choice, sampled: t };
438  }
439  return answer;
440}
441
442/**
443 * Jev arranges the song live. Jev is TypeSafe AI's System One model: it never writes text or music. It makes typed decisions over options the song wrote down, in about a tenth of a second, with calibrated probabilities. It can pick one option (`choice`), rate on a ladder (`score`), or say how likely a yes is (`noul`). The song composes those answers in code, so Jev only ever selects among what the song wrote.
444 *
445 * `jev({ state, questions })` is TypeSafe's request exactly, as the SDK (`@typesafe-ai/sdk`) sends it, and takes nothing else. `state` is what Jev judges: any object, usually the song's title, key, lyrics and what each section is. `questions` names each question, built with `choice`, `score` or `noul`. Every question goes to Jev in one request, and no question can see another's answer. To ask one question after another is settled, declare a second jev() and give its `.every` an `after`.
446 *
447 * `.every(cycles, options)` is Strudel's side, called once on what jev() returns. It asks Jev again every `cycles` cycles; each ask is a section. It returns the answers keyed by question name, each field a pattern with one value per section:
448 * - a choice: `.choice`, `.confidence`, `.probabilities`
449 * - a score: `.score`, `.confidence`, `.probabilities`
450 * - a noul: `.noul`
451 *
452 * The options to `.every`:
453 * - `fallback: { name: value }`: what plays before Jev answers, or instead of an unusable answer. A choice falls back to its first option unless named here; a score (0 to levels−1) and a noul (0 to 1) must be named.
454 * - `after: { name: answer }`: an answer from another jev() on the same cadence. This jev() is asked once that answer is settled, and sees it in its state as `decidedThisSection`.
455 * - `allow: { name: (decided) => [options] }`: narrows a choice per section from what `after` decided, plus `playingNow` (the section before) when that answer walks a form. A rule like "no riser out of a build" is then code, not a sentence Jev may ignore. The fallback must always be allowed.
456 * - `sample`: a temperature (or `{ name: t }`) to draw each choice from Jev's probabilities instead of always its top pick.
457 * - `minConfidence`: a threshold (or `{ name: c }`) under which a choice or score plays its fallback. Not together with `sample` on the same question.
458 * - `forget: [names]`: questions left out of the history Jev is sent.
459 * - `segments`: how many sections the song lasts, when no `walk` form ends it.
460 *
461 * Each request carries the song's `state`, the history of past answers, listeners' 🔥/😴 reactions (`listenersSoFar`), and, with a form, where the song is in it. The next section's questions go out halfway through the current one at the latest. A section that begins before its answer lands plays its fallbacks. An answer that is not one of the song's options plays the fallback, and the console says why.
462 *
463 * TypeSafe's advice holds here: "ask independent questions together, then compose their answers in code", and "keep control flow and deterministic rules in code" (see `walk`).
464 *
465 * All of Jev's text goes in 'single quotes'. The REPL turns "double quotes" and backticks into mini-notation, and jev() refuses them.
466 * @name jev
467 * @tags jev
468 * @param {Object} request `{ state, questions }`, TypeSafe's request
469 * @returns {Object} `{ every(cycles, options) }`, which returns the answers
470 * @example
471 * setcps(.5)
472 * const djJev = jev({
473 *   state: { title: 'NIGHT BUS', what: 'a chord pad over drums', key: 'A minor' },
474 *   questions: {
475 *     chord: choice('Pick the chord for the next four bars', {
476 *       home: 'A minor, where the song rests',
477 *       lift: 'F major, a lift',
478 *       tense: 'E major, pulling back home',
479 *     }),
480 *     energy: score('How hard should the drums hit?', ['soft: a kick a bar', 'steady: four on the floor']),
481 *     hats: noul('Should the next four bars add eighth-note hats?'),
482 *   },
483 * })
484 * const dj = djJev.every(4, { fallback: { energy: 1, hats: 0 }, segments: 8 })
485 * stack(
486 *   note(dj.chord.choice.pick({ home: "a2,c3,e3", lift: "f2,a2,c3", tense: "e2,g#2,b2" })).s('sawtooth').lpf(1000),
487 *   s(dj.energy.score.round().pick(["bd", "bd*4"])),
488 *   s("hh*8").mask(dj.hats.noul.gt(.5)),
489 * )
490 */
491// `decide(state, questions, { begins })` resolves to Jev's answers, keyed
492// like `questions`: the relay in the page (jev.mjs), a stub in tests.
493// `begins` is the cycle the section asked about begins at, so the page can
494// tell the relay how far ahead each call went out. `hear(from,
495// to)` summarises what the page actually played between two cycles
496// (meter.mjs), or null; `random` samples.
497export function createJev(decide, { hear = () => null, random = Math.random, listeners = () => null } = {}) {
498  return function jev({ state = {}, questions = {}, ...rest } = {}) {
499    if (Object.keys(rest).length) {
500      throw new Error(
501        `jev: it takes TypeSafe's state and questions only, not ${Object.keys(rest).join(', ')}; when to ask is .every(cycles, { fallback }), a form is walk(...)`,
502      );
503    }
504    if (state instanceof Pattern) text('`state`', state);
505    const checked = new Map(); // name → question, validated
506    for (const [name, q] of Object.entries(questions)) {
507      if (!/^[A-Za-z_]\w*$/.test(name)) throw new Error(`jev: "${name}" is not a usable question name`);
508      if (!q || !['choice', 'score', 'noul'].includes(q.type)) {
509        throw new Error(`jev: question "${name}" must be built with choice(), score() or noul()`);
510      }
511      if (q.instructions !== null) text(`${name}'s instructions`, q.instructions);
512      if (q.type === 'choice') {
513        const names = Object.keys(q.criteria ?? {});
514        if (names.length < 2) throw new Error(`jev: choice "${name}" needs at least two options`);
515        for (const o of names) if (q.criteria[o] !== null) text(`option "${o}" of ${name}`, q.criteria[o]);
516      } else if (q.type === 'score') {
517        const levels = q.criteria;
518        if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10) {
519          throw new Error(`jev: score "${name}" needs 2 to 10 levels, low to high`);
520        }
521        levels.forEach((l, i) => l !== null && text(`${name}'s level ${i}`, l));
522      } else if (q.criteria) {
523        for (const k of ['true', 'false']) q.criteria[k] != null && text(`${name}'s ${k}`, q.criteria[k]);
524      }
525      checked.set(name, q);
526    }
527    if (!checked.size) throw new Error('jev: give it `questions`');
528    let asked = false;
529
530    // Strudel's side: ask every `every` cycles, with these fallbacks.
531    // - fallback: what plays without a usable answer (see the header).
532    // - after: { name: answer } from another jev() on the same cadence:
533    //   this one is asked only once those are decided, with them in its
534    //   state, since questions in one request cannot see each other.
535    // - sample: a temperature (or { name: t }) to sample each choice from
536    //   Jev's probabilities instead of always taking its top pick.
537    // - minConfidence: a threshold (or { name: c }) under which a choice
538    //   or score plays its fallback instead. Not with `sample` on the same
539    //   question: sampling acts on the probabilities, and a close call is
540    //   exactly where it should differ from the top pick. TypeSafe: "If you
541    //   have a specific statistical algorithm in mind, you should probably
542    //   be using probabilities instead of confidence" (docs.typesafe.ai).
543    const everyFn = (
544      every,
545      {
546        fallback = {},
547        segments = Infinity,
548        after = {},
549        sample = null,
550        minConfidence = null,
551        allow = {},
552        forget = [],
553        ...more
554      } = {},
555    ) => {
556      if (Object.keys(more).length) {
557        throw new Error(
558          `jev: .every() takes fallback, after, sample, minConfidence, allow, forget and segments, not ${Object.keys(more).join(', ')}`,
559        );
560      }
561      for (const name of forget) {
562        if (!checked.has(name)) throw new Error(`jev: forget: "${name}" is not one of this jev()'s questions`);
563      }
564      if (asked) throw new Error('jev: one .every() per jev(); declare another jev() for another cadence');
565      asked = true;
566      if (!(every > 0)) throw new Error('jev: .every(cycles) needs a positive number of cycles');
567      if (!(segments > 0)) throw new Error('jev: `segments` must be a positive number (or left out)');
568
569      const qs = new Map();
570      for (const [name, q] of checked) {
571        let fb = fallback[name];
572        if (q.type === 'choice') {
573          const names = Object.keys(q.criteria);
574          fb ??= names[0];
575          if (!names.includes(fb)) throw new Error(`jev: ${name}'s fallback "${fb}" is not an option`);
576        } else if (q.type === 'score') {
577          if (!(typeof fb === 'number' && fb >= 0 && fb <= q.criteria.length - 1)) {
578            throw new Error(
579              `jev: score "${name}" needs a fallback from 0 to ${q.criteria.length - 1}: .every(n, { fallback: { ${name}: … } })`,
580            );
581          }
582        } else if (!(typeof fb === 'number' && fb >= 0 && fb <= 1)) {
583          throw new Error(`jev: noul "${name}" needs a fallback from 0 to 1: .every(n, { fallback: { ${name}: … } })`);
584        }
585        qs.set(name, { ...q, fallback: fb });
586      }
587      for (const name of Object.keys(fallback)) {
588        if (!qs.has(name)) throw new Error(`jev: fallback for "${name}", which is not a question`);
589      }
590      const perName = (option, name, what) => {
591        if (option === null || typeof option === 'number') return option;
592        for (const k of Object.keys(option))
593          if (!qs.has(k)) throw new Error(`jev: ${what} for "${k}", which is not a question`);
594        return option[name] ?? null;
595      };
596      const temperature = (name) => perName(sample, name, 'sample');
597      const threshold = (name) => perName(minConfidence, name, 'minConfidence');
598      for (const name of qs.keys()) {
599        if (temperature(name) && threshold(name) !== null) {
600          throw new Error(
601            `jev: "${name}" is sampled, so it cannot also have a minConfidence: sampling uses the probabilities`,
602          );
603        }
604      }
605      for (const [name, rule] of Object.entries(allow)) {
606        if (qs.get(name)?.type !== 'choice') throw new Error(`jev: allow.${name} must name a choice question`);
607        if (typeof rule !== 'function') throw new Error(`jev: allow.${name} must be a function of what is decided`);
608      }
609      const upstream = Object.entries(after).map(([name, answer]) => {
610        const owner = owners.get(answer);
611        if (!owner) throw new Error(`jev: after.${name} must be an answer from another jev().every(…)`);
612        if (owner.every !== every)
613          throw new Error(`jev: after.${name} is asked every ${owner.every} cycles, not ${every}`);
614        if (qs.has(name)) throw new Error(`jev: after.${name} has the name of one of this jev()'s questions`);
615        return { name, owner };
616      });
617
618      // Set by walk(): { name, sections: { [option]: { at, next, times } }, max, start, endings }
619      let form = null;
620      const limit = () => (form ? form.max : segments);
621      const cyclesOf = (s) => (every === 1 ? `${s + 1}` : `${s * every + 1}-${(s + 1) * every}`);
622
623      // ── what the page shows (boothStore.mjs) ────────
624      const view = {
625        model: JEV_MODEL,
626        about: state,
627        every,
628        get segments() {
629          return limit();
630        },
631        questions: [...qs].map(([name, q]) => ({
632          name,
633          type: q.type,
634          instructions: q.instructions,
635          criteria: q.criteria,
636        })),
637        form: null, // { name } once walk() attaches one
638        // the segment after which the song ends, once known: `segments` for a
639        // fixed length; with a form, the ending Jev picks
640        last: Number.isFinite(segments) ? segments - 1 : null,
641        decisions: [],
642        reactions: [],
643      };
644      view.index = boothStore.adopt(view);
645      // a recorded performance, when one is being replayed (setReplay)
646      const replayed = replaying ? { segments: replaying.jevs[view.index]?.segments ?? [] } : null;
647      view.replay = replaying ? { changed: replaying.changed } : null;
648      const changed = () => boothStore.changed(view);
649      // Who hears each settled decision (watchDecisions: a party's host).
650      view.watchers = new Set();
651      // Follow mode (followDecisions): decisions come from outside, through
652      // receive() below, and nothing is asked.
653      let following = false;
654      view.follow = () => {
655        following = true;
656        return receive;
657      };
658      view.react = (segment, kind) => {
659        const r = (view.reactions[segment] ??= { fire: 0, sleep: 0 });
660        r[kind] += 1;
661        changed();
662      };
663
664      const fallbacks = (sectionName) => {
665        const values = Object.fromEntries([...qs].map(([name, q]) => [name, fallbackAnswer(q, q.fallback)]));
666        if (form) values[form.name] = fallbackAnswer({ type: 'choice' }, sectionName);
667        return values;
668      };
669      view.decisions[0] = { segment: 0, cycles: cyclesOf(0), status: 'opening', values: fallbacks() };
670
671      // who is waiting for a segment to be decided (after:, in another jev)
672      const waiters = new Map();
673      const record = (segment, fields) => {
674        view.decisions[segment] = { ...view.decisions[segment], segment, ...fields };
675        changed();
676        if (fields.status !== 'asking') {
677          for (const watcher of view.watchers) watcher(segment, view.decisions[segment]);
678          for (const resolve of waiters.get(segment) ?? []) resolve(view.decisions[segment]);
679          waiters.delete(segment);
680        }
681      };
682      // Resolves to segment's decision once made, or null when the song
683      // will not reach it.
684      const decided = (segment) => {
685        request(segment);
686        const d = view.decisions[segment];
687        if (d && d.status !== 'asking') return Promise.resolve(d);
688        if (!d) return Promise.resolve(null);
689        return new Promise((resolve) => waiters.set(segment, [...(waiters.get(segment) ?? []), resolve]));
690      };
691      // Ask about the opening now, before the song starts (askOpening).
692      view.askOpening = () => decided(0);
693      // A row that played a fallback says so, so it does not read as Jev's
694      // choice; the opening and a forced section are the song's, not fallbacks.
695      const said = (d, k, how) => {
696        const a = d.values[k];
697        const text = describe(qs.get(k) ?? { type: 'choice' }, a, how);
698        return a.fallback && !a.forced && d.status !== 'opening' ? `${text} (fallback)` : text;
699      };
700      // What this jev() settled for a segment, as its history rows say it:
701      // forget: questions whose past answers Jev is not shown (it would copy
702      // them); a part absent from its section was never asked.
703      const remembered = (d, how) =>
704        Object.fromEntries(
705          Object.keys(d.values)
706            .filter((k) => !forget.includes(k) && !d.values[k]?.absent)
707            .map((k) => [k, said(d, k, how)]),
708        );
709      // The jev()s asked after this one (`after:`), each keyed by its view:
710      // what they settled for each section, for this one's history.
711      const arrangers = new Map();
712      const arrangementAt = (segment) => {
713        const rows = [...arrangers.values()].map(({ at }) => at(segment)).filter(Boolean);
714        return rows.length ? Object.assign({}, ...rows) : null;
715      };
716      const history = (upTo) =>
717        view.decisions.slice(0, upTo).map((d) => {
718          const arrangement = arrangementAt(d.segment);
719          return {
720            cycles: d.cycles,
721            ...(upstream.length
722              ? { decidedBefore: Object.fromEntries(upstream.map(({ name, owner }) => [name, owner.valueAt(d.segment)])) }
723              : {}),
724            ...remembered(d),
725            ...(arrangement ? { arrangement } : {}),
726          };
727        });
728      // Tell each jev() this one is asked after what it settles, once
729      // settled (a decision still out has not played yet, or plays its
730      // fallbacks until it lands), so its history can say how each section
731      // was arranged.
732      for (const { owner } of upstream) {
733        owner.arrangedBy(view, [...qs.keys()].filter((k) => !forget.includes(k)), (segment) => {
734          const d = view.decisions[segment];
735          if (!d || d.status === 'asking') return null;
736          const row = remembered(d, { brief: true });
737          return Object.keys(row).length ? row : null;
738        });
739      }
740      const audience = (upTo) =>
741        view.reactions
742          .map((r, s) => (r && s < upTo ? { cycles: cyclesOf(s), fire: r.fire, sleep: r.sleep } : null))
743          .filter(Boolean)
744          .slice(-4);
745
746      // The form's section in a segment: this jev's own form, or the one
747      // of a jev it is asked after; without a form, the segment's number.
748      const formOwner = () => (form ? null : upstream.map(({ owner }) => owner).find((o) => o.sectionNames()));
749      const sectionAt = (s) => {
750        if (form) return view.decisions[s]?.values[form.name]?.choice ?? null;
751        const owner = formOwner();
752        return owner ? owner.sectionAt(s) : String(s + 1);
753      };
754      // Every listener's tally for the sections that matter to segment's
755      // answer (the header's Listeners).
756      const listenersFor = (segment, next) => {
757        const tally = listeners();
758        if (!tally || typeof tally !== 'object') return null;
759        const names = form ? Object.keys(form.sections) : formOwner()?.sectionNames();
760        const known = (n) => typeof n === 'string' && (names ? names.includes(n) : /^\d+$/.test(n));
761        const recent = [];
762        for (let s = Math.max(0, segment - LISTENERS_BACK); s < segment; s++) recent.push(sectionAt(s));
763        const wanted = [...new Set([...recent, ...next])].filter((n) => known(n) && Object.hasOwn(tally, n));
764        const count = (x) => (Number.isInteger(x) && x > 0 ? x : 0);
765        const picked = wanted.slice(-LISTENERS_MAX).map((n) => [n, { fire: count(tally[n]?.fire), sleep: count(tally[n]?.sleep) }]);
766        return picked.length ? Object.fromEntries(picked) : null;
767      };
768
769      // ── asking ──────────────────────────────────────
770      const requested = new Set();
771      // With a form: the sections its rules allowed at each segment planned,
772      // which a decision from outside (receive) must also keep to.
773      const offered = new Map();
774      // Answers already heard before their section began: `${segment}:${name}`
775      // for a question a read ahead (`.early(1)`) played while its answer was
776      // still out. Only a query of a section that has not begun is one.
777      const readAhead = new Set();
778      // Playing time: the earliest cycle queried in the latest scheduler
779      // tick (the queries of one tick are synchronous, so a microtask ends
780      // it). A read ahead (`.early`) queries later cycles in the same tick,
781      // so it never moves the playhead into a section that has not begun.
782      let playhead = -Infinity;
783      let tickEarliest = null;
784      // Questions this tick read for a segment whose answer is still out;
785      // once the tick ends and its playing time is known, the ones for a
786      // segment that has not begun were read ahead.
787      let tickReads = [];
788      const heardAt = (cycle) => {
789        if (tickEarliest === null) {
790          tickEarliest = cycle;
791          queueMicrotask(() => {
792            for (const { segment, key } of tickReads) if (segment * every > tickEarliest) readAhead.add(key);
793            tickReads = [];
794            playhead = Math.max(playhead, tickEarliest);
795            tickEarliest = null;
796          });
797        } else tickEarliest = Math.min(tickEarliest, cycle);
798      };
799      const begun = (segment) => playhead >= segment * every;
800      const request = (segment) => {
801        if (requested.has(segment) || segment >= limit()) return;
802        if (view.last !== null && segment > view.last) return;
803        // The opening (segment 0) is asked before the song starts (askOpening);
804        // every later segment follows the one before it.
805        const opening = segment === 0;
806        // A segment first queried past its middle (a lagging or throttled tab)
807        // was never asked about; settle it first, so the chain has no gaps.
808        if (!opening && !view.decisions[segment - 1]) request(segment - 1);
809        const previous = opening ? null : view.decisions[segment - 1];
810        if (!opening && !previous) return;
811        // The section before ends the song (or will, if its answer is still
812        // out and its fallback is an ending): nothing to ask yet. Not marked
813        // requested, so an answer that goes on is asked about when it lands.
814        if (!opening && form && !form.sections[previous.values[form.name]?.choice]?.next.length) return;
815        requested.add(segment);
816
817        const asking = Object.fromEntries(
818          [...qs].map(([name, q]) => {
819            const { fallback: _, ...wire } = q;
820            return [name, wire];
821          }),
822        );
823        let planned = fallbacks();
824        let whereInForm = {};
825        let mayComeNext = form ? [] : [String(segment + 1)];
826        if (form) {
827          // the form's rules, in code: which sections may come next (the
828          // opening is always the form's start)
829          const current = previous?.values[form.name].choice;
830          // Only what may still play (`times`, counted over the sections
831          // before this one) and can still reach an ending by the cap: a
832          // section here plays, then at least toEnd[x] more, all by segment
833          // max - 1. Never empty: see stepsToEnd.
834          const plays = {};
835          for (let s = 0; s < segment; s++) {
836            const played = view.decisions[s]?.values[form.name]?.choice;
837            plays[played] = (plays[played] ?? 0) + 1;
838          }
839          const toEnd = stepsToEnd(
840            form.sections,
841            (x) => (plays[x] ?? 0) < (form.sections[x].times ?? Infinity),
842          );
843          const allowed = opening
844            ? [form.start]
845            : form.sections[current].next.filter((x) => segment + toEnd[x] <= form.max - 1);
846          offered.set(segment, allowed);
847          const written = opening ? form.start : writtenNext(form.sections, current, every);
848          const shortest = [...allowed].sort((a, b) => toEnd[a] - toEnd[b])[0];
849          const forced = allowed.length === 1 ? allowed[0] : null;
850          mayComeNext = allowed;
851          planned = fallbacks(forced ?? (allowed.includes(written) ? written : shortest));
852          if (forced) planned[form.name] = { ...planned[form.name], forced: true };
853          const q = qs.get(form.name);
854          if (forced) delete asking[form.name];
855          else asking[form.name] = choice(q.instructions, Object.fromEntries(allowed.map((n) => [n, q.criteria[n]])));
856          whereInForm = {
857            form: {
858              ...(opening ? { songStarts: true } : { playingNow: current }),
859              ...(forced ? { comesNext: forced } : { mayComeNext: allowed }),
860              endsAfter: form.endings,
861              sectionsLeft: form.max - segment,
862            },
863          };
864        }
865
866        const heard = audience(segment);
867        const measured = [];
868        for (let s = Math.max(0, segment - 4); s < segment - 1; s++) {
869          const m = hear(s * every, (s + 1) * every);
870          if (m) measured.push({ cycles: cyclesOf(s), ...m });
871        }
872        const sent = {
873          song: state,
874          segment: segment + 1,
875          cycles: cyclesOf(segment),
876          ...whereInForm,
877          history: history(segment),
878          ...(heard.length ? { audience: heard } : {}),
879          ...(measured.length ? { measuredSound: measured } : {}),
880        };
881
882        // A form's section, settled here or by receive(), is always one of
883        // form.sections, and one the form allowed there: every way in is
884        // checked. Jev's answer must be an own key of the narrowed choice
885        // (`asking`, the allowed sections), or the planned section plays;
886        // a sampled pick is kept only if it is one too; a replay keeps a
887        // recorded section only if `asking` offers it; a decision from
888        // outside (receive) only if it is the song's and `offered` there;
889        // and every fallback is the start or an allowed section. walk()
890        // makes sections and the choice's options the same set. So
891        // `form.sections[x]` is never undefined for a settled x, and the
892        // "reading 'next'" TypeError seen once (2026-09-25) cannot come
893        // from a live input: it was caught only in a hot-reloaded module
894        // version mid-refactor, when this line still read `.next` without
895        // `?.` (fixed in 101498b3); follow.test.mjs and jevCore.test.mjs
896        // feed foreign section names through each way in.
897        const settle = (fields) => {
898          if (form && form.sections[fields.values[form.name]?.choice]?.next.length === 0) {
899            view.last = Math.min(view.last ?? Infinity, segment);
900          }
901          record(segment, fields);
902        };
903
904        // The fallbacks are decided now, so a slow answer never leaves a gap.
905        record(segment, { cycles: sent.cycles, status: 'asking', state: replayed ? undefined : sent, values: planned });
906        // Following, the decision comes from outside (receive).
907        if (following) return;
908        if (!Object.keys(asking).length) {
909          // nothing to ask (a forced section and no other questions): no request
910          settle({ status: 'answered', values: planned, problems: [] });
911          return;
912        }
913        const t0 = Date.now();
914        const askedAt = playhead; // playing time when the request went out
915        Promise.all(upstream.map(({ owner }) => owner.decided(segment)))
916          .then((given) => {
917            if (given.some((d) => d === null))
918              throw Object.assign(new Error('the song ends before this section'), { ended: true });
919            const decidedNow = {};
920            if (upstream.length) {
921              sent.decidedThisSection = Object.fromEntries(
922                upstream.map(({ name, owner }, i) => {
923                  const a = given[i].values[owner.name];
924                  decidedNow[name] = owner.question.type === 'choice' ? a.choice : a[owner.question.type];
925                  const meaning = owner.question.criteria?.[a.choice];
926                  return [name, meaning ? `${a.choice}: ${meaning}` : describe(owner.question, a)];
927                }),
928              );
929              // where the song is, from an upstream form (this jev has none of its own)
930              if (!form) {
931                const where = upstream.map(({ owner }) => owner.where(segment)).find(Boolean);
932                if (where) {
933                  sent.form = where;
934                  decidedNow.playingNow = where.playingNow;
935                }
936              }
937            }
938            if (!replayed) {
939              // the section being entered, from the form this jev is asked after
940              const entering = form ? mayComeNext : [formOwner() ? sectionAt(segment) : String(segment + 1)];
941              const heardByAll = listenersFor(segment, entering.filter(Boolean));
942              if (heardByAll) sent.listenersSoFar = heardByAll;
943            }
944            // A part that does not play in this section as written is not
945            // asked about: it keeps its fallback, marked absent.
946            const span = writtenSpan(segment);
947            const parts = view.writtenParts;
948            if (parts && span) {
949              for (const name of Object.keys(asking)) {
950                if (parts.orbits.has(name) && !parts.playsIn(name, span)) {
951                  delete asking[name];
952                  planned = { ...planned, [name]: { ...planned[name], absent: true } };
953                }
954              }
955            }
956            // allow: narrow a choice to what the song's rules permit here
957            for (const [name, rule] of Object.entries(allow)) {
958              const q = qs.get(name);
959              const ok = [...new Set(rule(decidedNow) ?? [])].filter((o) => Object.hasOwn(q.criteria, o));
960              if (!ok.includes(q.fallback)) {
961                throw new Error(
962                  `allow.${name} leaves out its fallback "${q.fallback}"; a fallback must always be allowed`,
963                );
964              }
965              if (ok.length === 1) {
966                delete asking[name];
967                planned = { ...planned, [name]: { ...planned[name], forced: true } };
968              } else {
969                asking[name] = choice(q.instructions, Object.fromEntries(ok.map((o) => [o, q.criteria[o]])));
970              }
971            }
972            if (!Object.keys(asking).length || replayed) return {};
973            return decide(sent, asking, { begins: segment * every });
974          })
975          .then((answers = {}) => {
976            if (replayed) {
977              const fields = replay(segment, asking, planned);
978              settle(fields);
979              const said = Object.entries(fields.values).map(([k, a]) => `${k} ${describe(qs.get(k) ?? { type: 'choice' }, a)}`);
980              logger(`[jev] cycles ${sent.cycles} (replayed): ${said.join(', ')}`);
981              for (const p of [fields.reason, ...(fields.problems ?? [])].filter(Boolean)) {
982                logger(`[jev] cycles ${sent.cycles} (replayed): ${p}`, 'warning');
983              }
984              return;
985            }
986            const values = { ...planned };
987            const problems = [];
988            for (const [name, q] of Object.entries(asking)) {
989              const a = answers[name];
990              const unsure = (c) => {
991                const min = threshold(name);
992                if (min === null || !(typeof c === 'number') || c >= min) return false;
993                problems.push(
994                  `${name}: not confident enough (${Math.round(c * 100)}% < ${Math.round(min * 100)}%); played the fallback`,
995                );
996                return true;
997              };
998              if (q.type === 'choice') {
999                // own keys only: "toString" is `in` every object
1000                if (!Object.hasOwn(q.criteria, a?.choice)) {
1001                  problems.push(a ? `${name}: "${a.choice}" is not an option` : `${name}: no answer`);
1002                } else if (!unsure(a.confidence)) {
1003                  const t = temperature(name);
1004                  const picked = t ? sampleChoice(a, t, random) : a;
1005                  values[name] = Object.hasOwn(q.criteria, picked.choice) ? picked : a;
1006                }
1007              } else if (q.type === 'score') {
1008                if (typeof a?.score !== 'number') problems.push(`${name}: no score`);
1009                else if (!unsure(a.confidence)) {
1010                  values[name] = { ...a, score: Math.min(Math.max(a.score, 0), q.criteria.length - 1) };
1011                }
1012              } else if (typeof a?.noul === 'number') {
1013                values[name] = { ...a, noul: Math.min(Math.max(a.noul, 0), 1) };
1014              } else problems.push(`${name}: no answer`);
1015            }
1016            // A question read ahead keeps the fallback it already played; the
1017            // rest of the section still takes Jev's answers.
1018            for (const name of Object.keys(asking)) {
1019              if (readAhead.has(`${segment}:${name}`) && values[name] !== planned[name]) {
1020                values[name] = planned[name];
1021                problems.push(`${name}: answered after a read ahead played it; it kept the fallback`);
1022              }
1023            }
1024            let fields = { status: 'answered', values, problems, ms: Date.now() - t0 };
1025            // A segment already playing keeps its section, and takes the
1026            // rest of Jev's answers from the next whole cycle with at least
1027            // half a cycle to spare (the scheduler has queried a little
1028            // ahead); one that has no such cycle left plays its fallbacks.
1029            const from = Math.ceil(playhead + 0.5);
1030            const segmentEnd = (segment + 1) * every;
1031            if (begun(segment) && from < segmentEnd) {
1032              if (form) values[form.name] = planned[form.name];
1033              fields = {
1034                ...fields,
1035                values,
1036                planned,
1037                from,
1038                problems: [
1039                  ...problems,
1040                  `answered after the section began (asked at cycle ${cycleText(askedAt)}, answered at ${cycleText(playhead)}, ${Date.now() - t0} ms later); it plays from cycle ${from + 1}`,
1041                ],
1042              };
1043            } else if (begun(segment)) {
1044              fields = {
1045                ...fields,
1046                status: 'fallback',
1047                values: planned,
1048                problems: [
1049                  ...problems,
1050                  `answered after the section began (it begins at cycle ${segment * every}; asked at cycle ${cycleText(askedAt)}, answered at ${cycleText(playhead)}, ${Date.now() - t0} ms later); it played the fallbacks`,
1051                ],
1052              };
1053            }
1054            settle(fields);
1055            const said = Object.entries(fields.values).map(([k, a]) => `${k} ${describe(qs.get(k), a)}`);
1056            logger(`[jev] cycles ${sent.cycles}: ${said.join(', ')}`);
1057            for (const p of fields.problems) logger(`[jev] cycles ${sent.cycles}: ${p}`, 'warning');
1058          })
1059          .catch((e) => {
1060            if (e.ended) {
1061              // an upstream form ended the song first: so does this jev()
1062              view.last = Math.min(view.last ?? Infinity, segment - 1);
1063              record(segment, { status: 'fallback', reason: e.message, ended: true, values: planned });
1064              return;
1065            }
1066            settle({ status: 'fallback', reason: e.message, values: planned, ms: Date.now() - t0 });
1067            logger(`[jev] cycles ${sent.cycles}: ${e.message}; playing the fallbacks`, 'warning');
1068          });
1069      };
1070
1071      // A segment as the recorded performance played it, under the song's
1072      // rules now: `asking` holds what may be answered here (the form's
1073      // allowed sections, `allow`'s options), `planned` the fallbacks.
1074      const replay = (segment, asking, planned) => {
1075        const rec = replayed.segments[segment];
1076        if (!rec?.values) {
1077          return { status: 'fallback', values: planned, replayed: true, reason: 'not in the recorded performance' };
1078        }
1079        if (rec.status === 'fallback') {
1080          return { status: 'fallback', values: planned, replayed: true, reason: 'Jev could not answer when it was recorded' };
1081        }
1082        const values = { ...planned };
1083        const problems = [];
1084        for (const [name, q] of Object.entries(asking)) {
1085          const a = rec.values[name];
1086          const ok =
1087            q.type === 'choice'
1088              ? Object.hasOwn(q.criteria, a?.choice)
1089              : q.type === 'score'
1090                ? typeof a?.score === 'number' && a.score >= 0 && a.score <= q.criteria.length - 1
1091                : typeof a?.noul === 'number' && a.noul >= 0 && a.noul <= 1;
1092          if (ok) values[name] = { ...a, type: q.type };
1093          else problems.push(`${name}: ${a ? 'the recorded answer is not allowed here now' : 'not in the recorded performance'}; played the fallback`);
1094        }
1095        const fields = { status: 'answered', values, problems, replayed: true };
1096        // an answer that landed late played from a later cycle; it keeps its section
1097        const kept = !form || values[form.name].choice === planned[form.name].choice;
1098        return Number.isInteger(rec.from) && kept ? { ...fields, planned, from: rec.from } : fields;
1099      };
1100
1101      // ── following ───────────────────────────────────
1102      // A decision from outside is as untrusted as Jev's answer: each value
1103      // must be one this song's question allows, or the fallback plays.
1104      const cleanAnswer = (name, a) => {
1105        const q = qs.get(name);
1106        if (!q || !isObject(a)) return null;
1107        const flags = {
1108          ...(a.fallback === true ? { fallback: true } : {}),
1109          ...(a.forced === true ? { forced: true } : {}),
1110          ...(a.absent === true ? { absent: true } : {}),
1111        };
1112        const unit = (x) => typeof x === 'number' && x >= 0 && x <= 1;
1113        const confidence = unit(a.confidence) ? a.confidence : 0;
1114        const probabilities = (ok) =>
1115          isObject(a.probabilities)
1116            ? Object.fromEntries(Object.entries(a.probabilities).filter(([k, p]) => ok(k) && unit(p)))
1117            : {};
1118        if (q.type === 'choice') {
1119          if (!Object.hasOwn(q.criteria, a.choice)) return null;
1120          const own = (k) => Object.hasOwn(q.criteria, k);
1121          const sampled = typeof a.sampled === 'number' && a.sampled > 0 && own(a.top);
1122          return {
1123            type: 'choice',
1124            choice: a.choice,
1125            confidence,
1126            probabilities: probabilities(own),
1127            ...flags,
1128            ...(sampled ? { sampled: a.sampled, top: a.top } : {}),
1129          };
1130        }
1131        if (q.type === 'score') {
1132          if (typeof a.score !== 'number' || !Number.isFinite(a.score)) return null;
1133          const top = q.criteria.length - 1;
1134          return {
1135            type: 'score',
1136            score: Math.min(Math.max(a.score, 0), top),
1137            confidence,
1138            probabilities: probabilities((k) => /^\d+$/.test(k) && Number(k) <= top),
1139            ...flags,
1140          };
1141        }
1142        if (typeof a.noul !== 'number' || !Number.isFinite(a.noul)) return null;
1143        return { type: 'noul', noul: Math.min(Math.max(a.noul, 0), 1), ...flags };
1144      };
1145      // `allowed`: with a form, the sections its rules allow in this segment
1146      // (null when the segment was never planned, so nothing narrows it).
1147      const cleanValues = (values, base, allowed = null) => {
1148        const out = { ...base };
1149        const problems = [];
1150        for (const name of qs.keys()) {
1151          const a = cleanAnswer(name, values?.[name]);
1152          if (a && form && name === form.name && allowed && !allowed.includes(a.choice)) {
1153            problems.push(`${name}: "${a.choice}" may not play here under the song's form; it played the fallback`);
1154          } else if (a) out[name] = a;
1155          else problems.push(`${name}: the decision sent is not one of this song's; it played the fallback`);
1156        }
1157        return { values: out, problems };
1158      };
1159      const texts = (xs) => (Array.isArray(xs) ? xs.filter(isText).slice(0, 12).map((x) => x.slice(0, 300)) : []);
1160
1161      // One settled decision from outside, as shareDecision() makes it.
1162      // Returns whether it was taken: a segment already settled keeps what it
1163      // has, so a decision received twice (a reconnect) changes nothing.
1164      const receive = (segment, wire) => {
1165        if (!Number.isInteger(segment) || segment < 0 || segment >= limit() || !isObject(wire)) return false;
1166        // plan it first, as asking would, so its fallbacks are the song's
1167        // own for that place in the form (request records them, asks nothing)
1168        if (!view.decisions[segment]) request(segment);
1169        const current = view.decisions[segment];
1170        if (current && current.status !== 'asking' && current.status !== 'opening') return false;
1171        // the song ends where the sender's does
1172        if (wire.last === null || (Number.isInteger(wire.last) && wire.last >= 0 && wire.last < limit())) {
1173          view.last = wire.last;
1174        }
1175        const pending = current?.status === 'asking' ? current.values : null;
1176        const sectionSent = form && wire.values?.[form.name]?.choice;
1177        const base =
1178          pending ?? fallbacks(form ? (Object.hasOwn(form.sections, sectionSent) ? sectionSent : form.start) : undefined);
1179        // A segment planned here has the sections its form allowed (with the
1180        // song's `times` counted). One after a section still pending an
1181        // ending (received ahead of that section's own decision) was never
1182        // planned, so only the section's name is checked.
1183        const allowed = form ? (offered.get(segment) ?? null) : null;
1184        const { values, problems } = cleanValues(wire.values, base, allowed);
1185        // the sender's own late answer, cut where it cut it
1186        const cut =
1187          Number.isInteger(wire.from) && wire.from > segment * every && wire.from < (segment + 1) * every
1188            ? { from: wire.from, planned: cleanValues(wire.planned, base, allowed).values }
1189            : {};
1190        let fields = {
1191          cycles: cyclesOf(segment),
1192          status: wire.status === 'fallback' ? 'fallback' : 'answered',
1193          values,
1194          ...cut,
1195          problems: [...texts(wire.problems), ...problems],
1196          ...(isText(wire.reason) ? { reason: wire.reason.slice(0, 300) } : {}),
1197          ...(typeof wire.ms === 'number' && wire.ms >= 0 ? { ms: wire.ms } : {}),
1198          followed: true,
1199        };
1200        requested.add(segment);
1201        if (!begun(segment)) {
1202          // a question read ahead keeps the fallback it already played
1203          for (const name of qs.keys()) {
1204            if (pending && readAhead.has(`${segment}:${name}`) && fields.values[name] !== pending[name]) {
1205              fields.values = { ...fields.values, [name]: pending[name] };
1206              fields.problems = [
1207                ...fields.problems,
1208                `${name}: arrived after a read ahead played it; it kept the fallback`,
1209              ];
1210            }
1211          }
1212        } else {
1213          // It began before the decision arrived: a late answer. The section
1214          // stays as it began; the rest plays from the next whole cycle.
1215          const from = Math.max(Math.ceil(playhead + 0.5), cut.from ?? -Infinity);
1216          if (from < (segment + 1) * every) {
1217            const late = { ...fields.values };
1218            if (form) late[form.name] = base[form.name];
1219            fields = {
1220              ...fields,
1221              values: late,
1222              planned: base,
1223              from,
1224              problems: [...fields.problems, `arrived after the section began; it plays from cycle ${from + 1}`],
1225            };
1226          } else {
1227            fields = {
1228              ...fields,
1229              status: 'fallback',
1230              values: base,
1231              from: undefined,
1232              planned: undefined,
1233              problems: [...fields.problems, 'arrived after the section began; it played the fallbacks'],
1234            };
1235          }
1236        }
1237        record(segment, fields);
1238        const said = Object.entries(fields.values).map(([k, a]) => `${k} ${describe(qs.get(k), a)}`);
1239        logger(`[jev] cycles ${fields.cycles}, followed: ${said.join(', ')}`);
1240        for (const p of fields.problems) logger(`[jev] cycles ${fields.cycles}: ${p}`, 'warning');
1241        return true;
1242      };
1243
1244      // ── playing ─────────────────────────────────────
1245      // What each segment plays, fixed once it has been queried with a final
1246      // decision; a segment that begins while its request is out plays its
1247      // fallbacks to the end.
1248      // The song as written (asWritten): the opening, then each section's
1249      // written successor with its fallbacks, until the written ending.
1250      const writtenValues = [];
1251      const written = (n) => {
1252        if (n === 0) return view.decisions[0].values;
1253        if (n >= limit()) return null;
1254        if (!form) return fallbacks();
1255        if (writtenValues[n] === undefined) {
1256          const current = written(n - 1)?.[form.name]?.choice;
1257          const next =
1258            current && form.sections[current].next.length ? writtenNext(form.sections, current, every) : null;
1259          writtenValues[n] = next ? fallbacks(next) : null;
1260        }
1261        return writtenValues[n];
1262      };
1263
1264      // The cycles a segment's section occupies as written: its own form's
1265      // section, or the one an upstream form decided, or the segment itself.
1266      const writtenSpan = (segment) => {
1267        if (form) {
1268          const section = view.decisions[segment]?.values[form.name]?.choice;
1269          const at = form.sections[section]?.at;
1270          return at === undefined ? null : [at, at + every];
1271        }
1272        for (const { owner } of upstream) {
1273          const span = owner.writtenSpan?.(segment);
1274          if (span) return span;
1275        }
1276        return [segment * every, (segment + 1) * every];
1277      };
1278
1279      const locked = new Map();
1280      const valuesFor = (n) => {
1281        if (locked.has(n)) return locked.get(n);
1282        const d = view.decisions[n];
1283        if (d.status !== 'asking') locked.set(n, d.values);
1284        return d.values;
1285      };
1286
1287      // One hap per segment, whole; nothing plays after the song's end.
1288      // Segments are counted in playing time: inside an arrangement the
1289      // query arrives at a section's written position (see `offsets`).
1290      const perSegment = (value, name) =>
1291        new Pattern((st) => {
1292          const off = offsetNow();
1293          const haps = [];
1294          let begin = st.span.begin.add(off);
1295          const stop = st.span.end.add(off);
1296          const dry = writtenDepth > 0;
1297          if (!dry) heardAt(begin.valueOf());
1298          while (begin.lt(stop)) {
1299            const segment = begin.div(every).floor();
1300            const whole = new TimeSpan(segment.mul(every), segment.add(1).mul(every));
1301            const end = whole.end.min(stop);
1302            const n = segment.valueOf();
1303            if (dry) {
1304              const values = n >= 0 ? written(n) : null;
1305              if (values) {
1306                haps.push(
1307                  new Hap(
1308                    new TimeSpan(whole.begin.sub(off), whole.end.sub(off)),
1309                    new TimeSpan(begin.sub(off), end.sub(off)),
1310                    value(values, n),
1311                  ),
1312                );
1313              }
1314              begin = end;
1315              continue;
1316            }
1317            // Following from mid-song, a segment may have nothing yet: it
1318            // plays its fallbacks until its decision arrives.
1319            if (following && n >= 0 && !view.decisions[n]) request(n);
1320            if (end.gt(whole.begin.add(every / 2))) request(n + 1);
1321            const over = view.last !== null && n > view.last;
1322            if (n >= 0 && n < limit() && !over && view.decisions[n]) {
1323              const d = view.decisions[n];
1324              if (name && d.status === 'asking') tickReads.push({ segment: n, key: `${n}:${name}` });
1325              // an answer that landed mid-section plays from its `from` cycle
1326              const cut = d.from !== undefined && d.from > n * every ? Fraction(d.from) : null;
1327              const pieces = cut
1328                ? [
1329                    [whole.begin, cut, d.planned],
1330                    [cut, whole.end, d.values],
1331                  ]
1332                : [[whole.begin, whole.end, valuesFor(n)]];
1333              for (const [from, to, values] of pieces) {
1334                const partBegin = begin.max(from);
1335                const partEnd = end.min(to);
1336                if (!partBegin.lt(partEnd)) continue;
1337                haps.push(
1338                  new Hap(
1339                    new TimeSpan(from.sub(off), to.sub(off)),
1340                    new TimeSpan(partBegin.sub(off), partEnd.sub(off)),
1341                    value(values, n),
1342                  ),
1343                );
1344              }
1345            }
1346            begin = end;
1347          }
1348          return haps;
1349        });
1350
1351      const FIELDS = {
1352        choice: ['choice', 'confidence', 'probabilities'],
1353        score: ['score', 'confidence', 'probabilities'],
1354        noul: ['noul'],
1355      };
1356      const answers = {};
1357      for (const [name, q] of qs) {
1358        answers[name] = Object.fromEntries(
1359          FIELDS[q.type].map((f) => [f, perSegment((values) => values[name][f], name)]),
1360        );
1361        owners.set(answers[name], {
1362          name,
1363          question: q,
1364          every,
1365          decided,
1366          // what this question settled for a segment, for another jev()'s history
1367          valueAt(segment) {
1368            const a = view.decisions[segment]?.values[name];
1369            if (!a) return null;
1370            const v = q.type === 'choice' ? a.choice : describe(q, a);
1371            return a.fallback && !a.forced && view.decisions[segment].status !== 'opening' ? `${v} (fallback)` : v;
1372          },
1373          // a jev() asked after this one: its settled answers go into this
1374          // one's history rows (`arrangement`), so no two may share a name
1375          arrangedBy(by, names, at) {
1376            for (const [other, { names: taken }] of arrangers) {
1377              const both = other !== by && names.filter((n) => taken.includes(n));
1378              if (both?.length) {
1379                throw new Error(
1380                  `jev: two jev()s asked after "${name}" both have ${both.map((n) => `"${n}"`).join(', ')}; its history could not tell them apart`,
1381                );
1382              }
1383            }
1384            arrangers.set(by, { names, at });
1385          },
1386          // this form's section in `segment`, and every section it has
1387          sectionAt(segment) {
1388            return form && form.name === name ? (view.decisions[segment]?.values[name]?.choice ?? null) : null;
1389          },
1390          sectionNames() {
1391            return form && form.name === name ? Object.keys(form.sections) : null;
1392          },
1393          // the cycles `segment`'s section occupies as written, from this form
1394          writtenSpan(segment) {
1395            return form && form.name === name ? writtenSpan(segment) : null;
1396          },
1397          // where the song is in this question's form before `segment`
1398          where(segment) {
1399            if (!form || form.name !== name) return null;
1400            const now = view.decisions[segment - 1]?.values[name]?.choice;
1401            return now ? { playingNow: now, sectionsLeft: form.max - segment } : null;
1402          },
1403          // walk(): the form's rules over this choice
1404          attachForm(spec) {
1405            if (form) throw new Error('walk: this jev() already has a form');
1406            if (requested.size > 0) throw new Error('walk: attach the form before the song plays');
1407            form = { name, ...spec };
1408            // its sections in the order written, for pages (the song's tally)
1409            view.form = { name, sections: Object.keys(spec.sections).sort((a, b) => spec.sections[a].at - spec.sections[b].at) };
1410            view.last = spec.sections[spec.start].next.length === 0 ? 0 : null;
1411            view.decisions[0].values[name] = fallbackAnswer(q, spec.start);
1412            changed();
1413            return perSegment;
1414          },
1415        });
1416      }
1417      return answers;
1418    };
1419    return { every: everyFn };
1420  };
1421}
1422
1423/**
1424 * The song's form, as rules in code: which sections may follow which, and when the song ends. Jev picks each next section from a choice whose options are the sections; `walk` offers it only the sections allowed next. It makes the written order the fallback and ends the song. This is TypeSafe's "keep control flow and deterministic rules in code": Jev picks the section, and code decides what may be picked.
1425 *
1426 * Each section is one of the choice's options, exactly:
1427 * - `at`: the cycle it starts at in the song as written.
1428 * - `next`: the sections that may follow it. It must include the one that follows it as written; `[]` makes it an ending.
1429 * - `times` (optional): the most times it plays in one performance, counted in total, which bounds a loop.
1430 *
1431 * `max` caps the number of sections (16 by default); the song is steered to an ending by then. `start` is the first section (the first listed by default). `walk` refuses a form it cannot always finish.
1432 *
1433 * `form.arrange(pat)` plays each section's own cycles of `pat`: write the song out whole, in the written order, and the form replays its sections in the order Jev picks. A second jev() asked `after: { section }` then arranges each section, e.g. with part levels (`score`).
1434 * @name walk
1435 * @tags jev
1436 * @param {Object} answer a choice's answer from `.every(...)`, e.g. `section` in `const { section } = formJev.every(4)`
1437 * @param {Object} form `{ max = 16, start, sections: { name: { at, next, times } } }`
1438 * @returns {Object} `{ arrange(pat) }`: each section plays its own cycles of `pat`
1439 * @example
1440 * setcps(.5)
1441 * const formJev = jev({
1442 *   state: { title: 'SHORT FORM', what: 'a pad, then drums under it, then the pad alone to end' },
1443 *   questions: {
1444 *     section: choice('Pick the section that plays next; the outro ends the song', {
1445 *       intro: 'the pad alone',
1446 *       drop: 'the pad with the drums',
1447 *       outro: 'the pad fading out: the end',
1448 *     }),
1449 *   },
1450 * })
1451 * const { section } = formJev.every(4)
1452 * const form = walk(section, {
1453 *   max: 8,
1454 *   sections: {
1455 *     intro: { at: 0, next: ['drop'] },
1456 *     drop: { at: 4, next: ['drop', 'outro'], times: 3 },
1457 *     outro: { at: 8, next: [] },
1458 *   },
1459 * })
1460 * form.arrange(stack(
1461 *   note("<c3,e3,g3 a2,c3,e3 f2,a2,c3 g2,b2,d3>").s('sawtooth').lpf(900),
1462 *   s("bd*4, ~ sd").mask("<0!4 1!4 0!4>"),
1463 * ))
1464 */
1465export function walk(answer, { max = 16, start, sections, ...rest } = {}) {
1466  if (Object.keys(rest).length) throw new Error(`walk: unknown option ${Object.keys(rest).join(', ')}`);
1467  const owner = owners.get(answer);
1468  if (!owner) throw new Error("walk: give it a choice's answer from jev().every(), e.g. const { section } = formJev.every(8); walk(section, {...})");
1469  const { name, question } = owner;
1470  if (question.type !== 'choice') throw new Error(`walk: "${name}" must be a choice`);
1471  if (!(max > 0)) throw new Error("walk: a form's `max` must be a positive number of sections");
1472  const options = Object.keys(question.criteria);
1473  const all = Object.keys(sections ?? {});
1474  const missing = options.filter((o) => !all.includes(o));
1475  const extra = all.filter((o) => !options.includes(o));
1476  if (missing.length || extra.length) {
1477    throw new Error(
1478      `walk: the sections must be ${name}'s options exactly${missing.length ? `; missing ${missing.join(', ')}` : ''}${extra.length ? `; not options: ${extra.join(', ')}` : ''}`,
1479    );
1480  }
1481  for (const [n, sec] of Object.entries(sections)) {
1482    if (!(sec?.at >= 0)) throw new Error(`walk: section "${n}" needs \`at\`, the cycle it starts at`);
1483    if (!Array.isArray(sec.next)) throw new Error(`walk: section "${n}" needs \`next\`, a list (empty to end)`);
1484    for (const x of sec.next) {
1485      if (!Object.hasOwn(sections, x)) throw new Error(`walk: section "${n}" is followed by "${x}", not a section`);
1486    }
1487    const unknown = Object.keys(sec).filter((k) => !['at', 'next', 'times'].includes(k));
1488    if (unknown.length) throw new Error(`walk: section "${n}" takes at, next and times, not ${unknown.join(', ')}`);
1489    if (sec.times !== undefined && !(Number.isInteger(sec.times) && sec.times >= 1)) {
1490      throw new Error(`walk: section "${n}"'s \`times\` must be a whole number of plays, at least 1`);
1491    }
1492  }
1493  const first = start ?? all[0];
1494  if (!Object.hasOwn(sections, first)) throw new Error(`walk: the start "${first}" is not a section`);
1495  const endings = all.filter((n) => sections[n].next.length === 0);
1496  if (!endings.length) throw new Error('walk: the form needs an ending: a section with `next: []`');
1497  // How many sections must still play after each one to reach an ending:
1498  // each request offers only the next sections that can end the song by
1499  // the cap (and within each section's `times`, counted then), so the
1500  // ending is never forced onto a section mid-phrase. Every section may
1501  // play at least once, so here all are open.
1502  const toEnd = stepsToEnd(sections);
1503  const stuck = all.filter((n) => toEnd[n] === Infinity);
1504  if (stuck.length) throw new Error(`walk: no way to an ending from ${stuck.join(', ')}`);
1505  // The written order is the fallback, so it must always be allowed.
1506  for (const [n, sec] of Object.entries(sections)) {
1507    const w = writtenNext(sections, n, owner.every);
1508    if (w && !sec.next.includes(w))
1509      throw new Error(`walk: "${n}" is followed by "${w}" as written, so its \`next\` must include it`);
1510  }
1511  if (toEnd[first] > max - 1)
1512    throw new Error(`walk: the shortest way through the form is ${toEnd[first] + 1} sections, over the cap of ${max}`);
1513  // Every section, and every way on the form writes down, must be able to
1514  // play: a request offers x after n only when an ending is still in reach,
1515  // and n is never reached before its shortest way from the start. A way on
1516  // that can never be offered is a detour the song claims and Jev never
1517  // hears of (hey-listen's drop2d → drop1 was one, 2026-09-26).
1518  const fromStart = Object.fromEntries(all.map((n) => [n, n === first ? 0 : Infinity]));
1519  for (let changed = true; changed; ) {
1520    changed = false;
1521    for (const n of all) {
1522      for (const x of sections[n].next) {
1523        if (fromStart[n] + 1 < fromStart[x]) [fromStart[x], changed] = [fromStart[n] + 1, true];
1524      }
1525    }
1526  }
1527  for (const n of all) {
1528    if (fromStart[n] + toEnd[n] > max - 1) {
1529      throw new Error(`walk: "${n}" can never play: no way through it ends within the cap of ${max} sections`);
1530    }
1531    for (const x of sections[n].next) {
1532      if (fromStart[n] + 1 + toEnd[x] > max - 1) {
1533        throw new Error(
1534          `walk: "${n}" → "${x}" can never be taken: from there the song cannot end within the cap of ${max} sections`,
1535        );
1536      }
1537    }
1538  }
1539  const perSegment = owner.attachForm({ sections, max, start: first, endings });
1540  return {
1541    arrange(pat) {
1542      return perSegment((values, n) => n * owner.every - sections[values[name].choice].at)
1543        .fmap((shift) => (shift ? shifted(pat, shift) : pat))
1544        .innerJoin();
1545    },
1546  };
1547}