playthrough 0.5.1 → 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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON
2
- import { A as quote, C as inOrderJoined, D as roundInProgress, E as problemsFinishing, O as toAccountFor, S as conversation, T as notesToAnswer, g as readingsOn, j as time, k as list, p as firstLine, w as lastSeen, x as allNotes, y as Refused } from "./reporter-S9fOk0za.js";
3
- import { A as repo, M as thisCommand, N as pausedAt, O as setupIn, S as stopApp, T as giveBackLeft, _ as compareWithEarlier, a as failedAt, b as keepRunning, c as isFlow, d as runFlows, g as writtenNow, h as trySetup, i as called, j as stillAt, k as copies, l as makeFlowsFolder, m as whereItStopped, o as flowsOf, p as walkFork, s as howToWriteFlows, t as ALONGSIDE, u as recorder, v as placeIn, x as serves, y as shownAt } from "./flows-vi0amwSo.js";
2
+ import { A as quote, C as inOrderJoined, D as roundInProgress, E as problemsFinishing, O as toAccountFor, S as conversation, T as notesToAnswer, g as readingsOn, j as time, k as list, p as firstLine, w as lastSeen, x as allNotes, y as Refused } from "./reporter-DlS7qehW.js";
3
+ import { A as setupIn, C as serves, D as giveBackLeft, F as pausedAt, M as repo, N as stillAt, P as thisCommand, S as keepRunning, _ as compareWithEarlier, a as failedAt, c as isFlow, d as runFlows, f as runFork, g as writtenNow, h as trySetup, i as called, j as copies, l as makeFlowsFolder, m as whereItStopped, o as flowsOf, s as howToWriteFlows, t as ALONGSIDE, u as recorder, v as firstShown, w as stopApp, x as shownAt, y as placeIn } from "./flows-CbrwgoHB.js";
4
4
  import { t as read } from "./read-Bg9XVTtn.js";
5
5
  import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { execFileSync } from "node:child_process";
@@ -13,9 +13,9 @@ import { chromium, request } from "@playwright/test";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { createServer as createServer$1, request as request$1 } from "node:http";
15
15
  // src/node.ts
16
- const [major] = process.versions.node.split(".").map(Number);
17
- if (major < 24) {
18
- console.error(`Playthrough needs Node 24 or later, and this is Node ${process.versions.node}. Install Node 24, then run this again.`);
16
+ const [major, minor] = process.versions.node.split(".").map(Number);
17
+ if (major < 22 || major === 22 && minor < 18) {
18
+ console.error(`Playthrough needs Node 22.18 or later, and this is Node ${process.versions.node}. Install Node 24, then run this again.`);
19
19
  process.exit(1);
20
20
  }
21
21
  // ../page/src/scheme.ts
@@ -328,6 +328,7 @@ function putWorkBack(repo, aside, force) {
328
328
  "--",
329
329
  "playthrough"
330
330
  ]);
331
+ if (force) keepWhatChanged(repo);
331
332
  git(repo, [
332
333
  "checkout",
333
334
  "--quiet",
@@ -348,6 +349,33 @@ function putWorkBack(repo, aside, force) {
348
349
  return said(error);
349
350
  }
350
351
  }
352
+ function keepWhatChanged(repo) {
353
+ const changed = git(repo, [
354
+ "status",
355
+ "--porcelain",
356
+ "--untracked-files=all"
357
+ ]).split("\n").filter(Boolean).map((line) => line.replace(/^\s*\S+\s+/, ""));
358
+ if (!changed.length) return;
359
+ const kept = `Playthrough: saved while flows recorded on Before, ${(/* @__PURE__ */ new Date()).toISOString()}`;
360
+ git(repo, [
361
+ "stash",
362
+ "push",
363
+ "--include-untracked",
364
+ "--quiet",
365
+ "--message",
366
+ kept
367
+ ], {
368
+ ...process.env,
369
+ ...PLAYTHROUGH
370
+ });
371
+ const ref = git(repo, [
372
+ "stash",
373
+ "list",
374
+ "--format=%gd %gs"
375
+ ]).split("\n").find((stash) => stash.endsWith(kept))?.split(" ")[0];
376
+ const files = changed.length > 3 ? `${changed.slice(0, 3).join(", ")} and ${changed.length - 3} more` : changed.join(", ");
377
+ console.log(`\nWhile flows recorded on Before, ${files} changed, in the app as it was. That's kept, not lost: git stash pop ${ref} puts it over your work, if it's something you saved.`);
378
+ }
351
379
  function stillAside(repo, aside, problem) {
352
380
  const stash = stashOf(repo, aside);
353
381
  return new Refused([
@@ -427,7 +455,7 @@ async function runTo(repo, id, to) {
427
455
  order(one, resting);
428
456
  const { handed, stopped, made } = await one.ran;
429
457
  if (handed) {
430
- if (one.passed.length > passed && !to.did?.length) timesOf(repo, id, one).handing.set(momentOf(to.step, to.actions), Date.now() - one.last.when);
458
+ if (one.passed.length > passed && !to.did?.length && !to.notes) timesOf(repo, id, one).handing.set(momentOf(to.step, to.actions), Date.now() - one.last.when);
431
459
  return {
432
460
  handed,
433
461
  made
@@ -547,7 +575,7 @@ async function start$2(repo, id, to, files, written) {
547
575
  borrowed: repo.borrowed,
548
576
  arrived,
549
577
  person: kept.use.person
550
- }, kept.body, orders).catch((error) => ({ stopped: { why: firstLine(String(error?.message ?? error)) } }));
578
+ }, kept.body, orders, kept.forks).catch((error) => ({ stopped: { why: firstLine(String(error?.message ?? error)) } }));
551
579
  one.ran.finally(() => (one.over = true, close()));
552
580
  one.ran.then(({ handed, made }) => handed ? void 0 : made?.giveBack());
553
581
  context.on("close", one.stop);
@@ -589,6 +617,7 @@ function loadedOf(repo, id, files, written) {
589
617
  const kept = loaded.get(key(repo, id));
590
618
  if (kept?.written === written) return kept.flows;
591
619
  const load = (file, by) => by(file, written).catch((error) => {
620
+ if (error instanceof Refused) throw error;
592
621
  throw new Refused([`${repo.show(file)} couldn't be loaded, because Node says “${firstLine(String(error?.message ?? error))}”, so no flow of ${id} can be taken over. Fix it, then take the flow over again.`]);
593
622
  });
594
623
  const flows = alongside().then(({ flowsIn }) => Promise.all([Promise.all(files.map((file) => load(file, flowsIn))), load(repo.setup, setupIn)])).then(([each, setup]) => {
@@ -634,7 +663,7 @@ async function takeOver$1(repo, id, flowName, stepName) {
634
663
  Ready in ${seconds}s. Carry on from there in your browser: ${here}${away.length ? `\nOr on your phone, on the same network: ${list(away)}` : ""}
635
664
  Each address hands over the same copy, and the app is passed on through Playthrough while this runs. Ctrl+C ends it.`);
636
665
  }
637
- async function getReady(repo, id, flowName, { step: stepName, actions, did, ahead, network } = {}) {
666
+ async function getReady(repo, id, flowName, { step: stepName, actions, did, notes, ahead, network } = {}) {
638
667
  const round = await recorded(repo, id);
639
668
  const flow = round.flows.find((flow) => flow.name === flowName);
640
669
  if (!flow) throw new Refused([`${id} has no flow called ${quote(flowName)}. Its flows: ${list(round.flows.map((flow) => quote(flow.name)))}.`]);
@@ -651,7 +680,8 @@ async function getReady(repo, id, flowName, { step: stepName, actions, did, ahea
651
680
  step: stepName,
652
681
  ...actions && { actions }
653
682
  },
654
- ...did?.length && { did }
683
+ ...did?.length && { did },
684
+ ...notes?.length && { notes }
655
685
  });
656
686
  if (!passings.has(app.url)) {
657
687
  const moments = /* @__PURE__ */ new Map();
@@ -867,7 +897,8 @@ const SAYS = `<script>(() => {
867
897
  for (let at = target; at && at !== (pressed ?? document.body); at = at.parentElement) if (at.nodeType === 1 && words(at)) return at;
868
898
  return pressed ?? target;
869
899
  };
870
- const words = (element) => (element.getAttribute?.("aria-label") || element.innerText || element.value || element.placeholder || "").trim().replace(/\\s+/g, " ").slice(0, 80);
900
+ // The words you saw on it, as you'd name it; its label only when it shows none, as an icon's.
901
+ const words = (element) => (element.innerText?.trim() || element.getAttribute?.("aria-label") || element.value || element.placeholder || "").trim().replace(/\\s+/g, " ").slice(0, 80);
871
902
  const roles = { A: "link", BUTTON: "button", SELECT: "combobox", TEXTAREA: "textbox", H1: "heading", H2: "heading", H3: "heading", H4: "heading", LI: "listitem", TD: "cell", TH: "columnheader", LABEL: "text", P: "text" };
872
903
  const role = (element) => element.getAttribute("role") || (element.tagName === "INPUT" ? (["checkbox", "radio"].includes(element.type) ? element.type : "textbox") : roles[element.tagName]) || "text";
873
904
  const path = (element) => {
@@ -938,11 +969,14 @@ const SAYS = `<script>(() => {
938
969
  e.stopImmediatePropagation();
939
970
  if (e.type === "pointerdown") {
940
971
  const on = thing(e.target);
941
- const name = words(on);
942
972
  readNow();
943
973
  send();
944
974
  const node = window.rrwebRecord?.record?.mirror?.getId(on);
945
- parent.postMessage({ playthrough: "noted", ...(name ? { element: { role: role(on), name, css: path(on), ...(node > 0 && { node }) } } : { point: { x: e.clientX / innerWidth, y: e.clientY / innerHeight } }), did, x: e.clientX, y: e.clientY }, "*");
975
+ // Named as the page is read for every flow, so a later round's flow finds it by the same name: a reminder's
976
+ // message box is "Message" there, where its own words named it here.
977
+ const known = node > 0 && recording ? read().things.find((thing) => thing.node === node) : undefined;
978
+ const [kind, name] = known ? [known.role, known.name] : [role(on), words(on)];
979
+ parent.postMessage({ playthrough: "noted", ...(name ? { element: { role: kind, name, css: path(on), ...(node > 0 && { node }) } } : { point: { x: e.clientX / innerWidth, y: e.clientY / innerHeight } }), did, x: e.clientX, y: e.clientY }, "*");
946
980
  noting = false;
947
981
  swallowing = true;
948
982
  ringed();
@@ -1644,11 +1678,12 @@ async function live(repo, id, request, response) {
1644
1678
  response.end();
1645
1679
  }
1646
1680
  async function takeOver(repo, id, request, response) {
1647
- const { flow, step, actions, did, ahead } = await body(request);
1681
+ const { flow, step, actions, did, notes, ahead } = await body(request);
1648
1682
  const { port, moment } = await getReady(repo, id, flow, {
1649
1683
  step,
1650
1684
  actions,
1651
1685
  did,
1686
+ notes,
1652
1687
  ahead
1653
1688
  });
1654
1689
  json(response, 200, {
@@ -1809,9 +1844,9 @@ const known = [
1809
1844
  function agent(written, shell = process.env) {
1810
1845
  return known.find((agent) => agent.says.some((name) => shell[name]))?.name ?? written ?? "Agent";
1811
1846
  }
1812
- // src/walk.ts
1813
- const forkOf = (recording) => recording.match(/^(?:branches\/(.+)-\d+|walks\/(.+))\.json$/)?.slice(1).find(Boolean) ?? recording.replace(/\W+/g, "-");
1814
- async function walkForks(repo, review, round, flows) {
1847
+ // src/forks.ts
1848
+ const forkOf = (recording) => recording.match(/^(?:branches\/(.+)-\d+|forks\/(.+))\.json$/)?.slice(1).find(Boolean) ?? recording.replace(/\W+/g, "-");
1849
+ async function runForks(repo, review, round, flows) {
1815
1850
  const everyNote = allNotes(review);
1816
1851
  const open = notesToAnswer(review, round).flatMap((id) => {
1817
1852
  const kept = everyNote.get(id);
@@ -1821,103 +1856,123 @@ async function walkForks(repo, review, round, flows) {
1821
1856
  }] : [];
1822
1857
  });
1823
1858
  const forks = Map.groupBy(open, ({ note }) => forkOf(note.on.app.recording));
1824
- const walked = [];
1859
+ const ran = [];
1825
1860
  for (const [fork, notes] of forks) {
1826
1861
  const from = notes[0].note.on.app.from;
1827
1862
  const then = review.rounds.find((kept) => kept.number === notes[0].round)?.flows.find((flow) => flow.name === from.flow);
1828
1863
  const now = flows.find((flow) => flow.name === from.flow);
1829
1864
  const paused = then && pausedAt(then, from.at);
1830
- const unwalked = (stopped) => notes.map(({ id }) => ({
1865
+ const unrun = (failed) => notes.map(({ id }) => ({
1831
1866
  note: id,
1832
1867
  flow: from.flow,
1833
- reached: false,
1834
- stopped
1868
+ found: false,
1869
+ failed: { why: failed }
1835
1870
  }));
1836
1871
  if (!now || !paused) {
1837
- walked.push(...unwalked(`${quote(from.flow)} isn't in round ${round}`));
1872
+ ran.push(...unrun(`${quote(from.flow)} isn't in round ${round}`));
1838
1873
  continue;
1839
1874
  }
1840
- const did = notes.map(({ note }) => note.on.app.did).reduce((longest, one) => one.length > longest.length ? one : longest);
1841
- console.log(`\nWalking ${quote(from.flow)} again, as ${list([...new Set(notes.map(({ note }) => quote(note.id)))])} left it in the app…`);
1842
- const run = await walkFork(repo, review.id, {
1875
+ console.log(`\nRunning ${quote(from.flow)} to where ${list([...new Set(notes.map(({ note }) => quote(note.id)))])} took it over, then its fork…`);
1876
+ const run = await runFork(repo, review.id, {
1843
1877
  flow: from.flow,
1844
1878
  step: paused.step,
1845
1879
  actions: paused.actions,
1846
- did,
1847
- look: notes.map(({ note }) => ({
1848
- after: note.on.app.did.length,
1849
- css: note.on.element?.css
1850
- }))
1880
+ notes: notes.map(({ id }) => id)
1851
1881
  });
1852
1882
  if (!run) {
1853
- walked.push(...unwalked(`${quote(from.flow)} doesn't get to where they took it over`));
1883
+ ran.push(...unrun(`${quote(from.flow)} doesn't get to where they took it over, the step ${quote(paused.step)}`));
1884
+ continue;
1885
+ }
1886
+ const step = paused.step;
1887
+ if (!run.fork || run.failed) {
1888
+ ran.push(...notes.map(({ id }) => ({
1889
+ note: id,
1890
+ flow: from.flow,
1891
+ step,
1892
+ found: false,
1893
+ ...run.fork && { fork: run.fork },
1894
+ ...run.failed && { failed: run.failed }
1895
+ })));
1854
1896
  continue;
1855
1897
  }
1856
- const recording = `walks/${fork}.json`;
1898
+ const recording = `forks/${fork}.json`;
1857
1899
  const start = run.events[0].timestamp;
1858
- const length = run.stopped ? Math.round(((run.done.at(-1) ?? run.from) + .08) * 1e3) / 1e3 : Math.round(run.events.at(-1).timestamp - start) / 1e3;
1900
+ const length = Math.round(run.events.at(-1).timestamp - start) / 1e3;
1859
1901
  await repo.reviews.saveFile(review.id, round, recording, Buffer.from(JSON.stringify(run.events)));
1860
1902
  await repo.reviews.saveFile(review.id, round, `readings/${recording}`, Buffer.from(JSON.stringify(run.readings)));
1861
1903
  await repo.reviews.saveFile(review.id, round, `assets/${recording}`, Buffer.from(JSON.stringify(run.assets)));
1862
1904
  for (const { name, bytes } of run.files) await repo.reviews.saveFile(review.id, round, name, bytes);
1863
- const app = {
1864
- from: {
1865
- flow: from.flow,
1866
- at: run.from
1867
- },
1868
- did: did.slice(0, run.done.length),
1869
- recording,
1870
- length
1871
- };
1872
- for (const [i, { id, note }] of notes.entries()) {
1873
- const steps = note.on.app.did.length;
1874
- const reached = run.done.length >= steps;
1875
- const at = (reached ? run.done[steps - 1] : run.done.at(-1)) ?? run.from;
1876
- const node = reached ? run.found[i] : null;
1905
+ const picture = run.picture && await keepPicture(repo, review.id, round, `notes/${fork}-fork.png`, run.picture);
1906
+ const seen = [...run.readings, ...run.glimpses].filter((glimpse) => glimpse.at >= run.from);
1907
+ for (const { id, note } of notes) {
1908
+ const app = {
1909
+ from: {
1910
+ flow: from.flow,
1911
+ at: run.from
1912
+ },
1913
+ did: note.on.app.did,
1914
+ recording,
1915
+ length,
1916
+ steps: run.steps
1917
+ };
1918
+ const shown = note.on.element && firstShown(note.on.element, seen);
1877
1919
  const on = {
1878
1920
  flow: from.flow,
1879
- at,
1880
- ...node && note.on.element && { element: {
1881
- role: note.on.element.role,
1882
- name: note.on.element.name,
1883
- node
1884
- } },
1921
+ at: shown ? shown.at : Math.max(run.from, length - .1),
1922
+ ...shown && { element: shown.element },
1885
1923
  app
1886
1924
  };
1887
- const picture = run.pictures[i] && await keepPicture(repo, review.id, round, `notes/${id}-walked.png`, run.pictures[i]);
1888
- walked.push({
1925
+ ran.push({
1889
1926
  note: id,
1890
1927
  flow: from.flow,
1928
+ step,
1929
+ fork: run.fork,
1891
1930
  on,
1892
- reached,
1893
- ...!reached && { stopped: run.stopped ?? "" },
1931
+ found: !!shown,
1894
1932
  ...picture && { picture }
1895
1933
  });
1896
1934
  }
1897
1935
  }
1898
- return walked;
1936
+ return ran;
1899
1937
  }
1900
1938
  async function keepPicture(repo, id, round, name, bytes) {
1901
1939
  await repo.reviews.saveFile(id, round, name, bytes);
1902
1940
  return repo.show(join(dirname(repo.round(id, round)), "files", name));
1903
1941
  }
1904
- function saidOf(walked) {
1905
- const fork = `the steps note ${walked.note} was made after, on ${quote(walked.flow)}`;
1906
- if (walked.reached) return `Walked ${fork} again: they get to the note.${walked.picture ? ` As it is now: ${walked.picture}` : ""}`;
1907
- if (walked.on) return `Walked ${fork} again: they stop at ${quote(walked.stopped ?? "")}, which they can't do now, and the person sees that there. Fix what it's on, or say in your answer to note ${walked.note} why their steps changed.`;
1908
- return `Couldn't walk ${fork} again: ${walked.stopped}.`;
1942
+ function unfinished(ran) {
1943
+ const unwritten = ran.filter((one) => !one.fork && one.step);
1944
+ const lines = ran.filter((one) => one.fork && one.failed).map(({ note, fork, failed }) => `The fork ${quote(fork)} for note ${note} failed${failed.at ? ` at its step ${quote(failed.at)}` : ""}, where Playwright says “${failed.why}”. Fix it and record again.`);
1945
+ for (const [flow, notes] of Map.groupBy(unwritten, (one) => one.flow)) {
1946
+ const ids = notes.map((one) => one.note);
1947
+ lines.push(`${ids.length > 1 ? `Notes ${list(ids)} were` : `Note ${ids[0]} was`} made in the app, and ${ids.length > 1 ? "have" : "has"} no fork yet. Write what the person did there as a fork of ${quote(flow)}, after it in its file, finding things by role and name as its steps do:
1948
+
1949
+ test.fork("<what they do there>", { notes: [${ids.map((id) => `"${id}"`).join(", ")}] }, async ({ page }) => {
1950
+ await test.step("<a step of theirs>", async () => { … });
1951
+ });
1952
+
1953
+ It runs from where they took the flow over, in its step ${quote(notes[0].step)}, every round until the note is settled, and your answer goes on what the note is on, where the fork first shows it. Then record again.`);
1954
+ }
1955
+ return lines;
1956
+ }
1957
+ function saidOf(ran) {
1958
+ if (!ran.fork) return `Couldn't run note ${ran.note}'s fork: ${ran.failed?.why}.`;
1959
+ const noted = ran.on?.element ? `the ${ran.on.element.role} ${quote(ran.on.element.name)}` : "what it's on";
1960
+ if (ran.found) return `Ran ${quote(ran.fork)}, note ${ran.note}'s fork: it shows ${noted}, and your answer is there.${ran.picture ? ` Where it ends: ${ran.picture}` : ""}`;
1961
+ return `Ran ${quote(ran.fork)}, note ${ran.note}'s fork: it never shows what the note is on, so your answer is at its end.${ran.picture ? ` Where it ends: ${ran.picture}` : ""}`;
1909
1962
  }
1910
- async function walk(repo, id) {
1963
+ async function forks(repo, id) {
1911
1964
  const review = await repo.reviews.get(id);
1912
- if (review.accepted) throw new Refused(["This review is accepted, so there's nothing left to walk."]);
1965
+ if (review.accepted) throw new Refused(["This review is accepted, so there's no fork left to run."]);
1913
1966
  const number = roundInProgress(review);
1914
1967
  const saved = review.rounds.find((round) => round.number === number);
1915
- const walked = await walkForks(repo, review, number, saved?.flows ?? review.rounds.findLast((round) => round.number > 0)?.flows ?? []);
1916
- if (!walked.length) return console.log(`Nothing to walk: round ${number} answers no note made in the app.`);
1968
+ const ran = await runForks(repo, review, number, saved?.flows ?? review.rounds.findLast((round) => round.number > 0)?.flows ?? []);
1969
+ if (!ran.length) return console.log(`Nothing to run: round ${number} answers no note made in the app.`);
1970
+ const left = unfinished(ran);
1971
+ if (left.length) throw new Refused(left);
1917
1972
  const now = saved && (await repo.reviews.get(id)).rounds.find((round) => round.number === number);
1918
1973
  if (now) {
1919
1974
  const placed = (reply) => {
1920
- const on = walked.find((one) => one.note === reply.note)?.on;
1975
+ const on = ran.find((one) => one.note === reply.note)?.on;
1921
1976
  return on ? {
1922
1977
  ...reply,
1923
1978
  on
@@ -1928,7 +1983,7 @@ async function walk(repo, id) {
1928
1983
  replies: now.replies.map(placed)
1929
1984
  });
1930
1985
  }
1931
- console.log(`\n${walked.map(saidOf).join("\n")}`);
1986
+ console.log(`\n${ran.map(saidOf).join("\n")}`);
1932
1987
  }
1933
1988
  // src/record.ts
1934
1989
  async function record(repo, id) {
@@ -1960,8 +2015,10 @@ async function record(repo, id) {
1960
2015
  words: ""
1961
2016
  }));
1962
2017
  const by = agent(saved?.by);
1963
- const walked = await walkForks(repo, review, number, flows);
1964
- const replies = await Promise.all([...written, ...waiting].map((reply) => placed(repo, review, number, flows, reply, walked)));
2018
+ const forked = await runForks(repo, review, number, flows);
2019
+ const forkless = unfinished(forked);
2020
+ if (forkless.length) throw new Refused([`\nRound ${number} wasn't recorded:`, ...forkless]);
2021
+ const replies = await Promise.all([...written, ...waiting].map((reply) => placed(repo, review, number, flows, reply, forked)));
1965
2022
  console.log(`\nComparing round ${number} with each round before it…`);
1966
2023
  const comparisons = await compareWithEarlier(repo.reviews, id, number, flows);
1967
2024
  await repo.reviews.saveRound(id, {
@@ -1996,7 +2053,7 @@ async function record(repo, id) {
1996
2053
  console.log(`${parts} only ${look} different in the pictures, with nothing read there changed${pictured.length > 3 ? "" : `: ${list(pictured.map((change) => quote(change.words)))}`}. The person sees ${them} on the recording, and ${need} nothing from you, unless it's what you changed for a note: then say so in “accounts”, and it shows in your answer.`);
1997
2054
  }
1998
2055
  if (since && !since.changes.length && replies.some((reply) => reply.on && !reply.on.app && !reply.watch)) console.log(`Nothing at rest shows an answer that moves, like an animation: if yours does, add "watch": "<the title of the step it's seen in>" to its reply in ${file}, and the note plays that step.`);
1999
- if (walked.length) console.log(`\n${walked.map(saidOf).join("\n")}`);
2056
+ if (forked.length) console.log(`\n${forked.map(saidOf).join("\n")}`);
2000
2057
  if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
2001
2058
  if (number === 1 && !saved && named.length) console.log(`\nSay why each change is there in “accounts”, by its id: { "line": "<why, in your words>" }, { "note": "<the id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
2002
2059
  const wrote = number === 1 && !saved && recorded.find((kept) => kept.wrote);
@@ -2009,21 +2066,21 @@ ${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it ho
2009
2066
  if (by === "Agent") console.log(`\nPlaythrough can't tell which agent you are. Write your name in ${file} as “by”, and each of your answers shows it.`);
2010
2067
  console.log(`\n${left.length ? "Then hand it over" : "Hand it over when it's ready"}, in the background, so you're told when the person's notes come: playthrough wait ${id}`);
2011
2068
  }
2012
- async function placed(repo, review, number, flows, reply, walked) {
2069
+ async function placed(repo, review, number, flows, reply, forked) {
2013
2070
  const answer = {
2014
2071
  note: reply.note,
2015
2072
  words: reply.words,
2016
2073
  ...reply.watch && { watch: reply.watch }
2017
2074
  };
2018
- const seen = lastSeen(review, reply.note, number);
2019
- const walk = seen?.on.app && walked.find((one) => one.note === reply.note)?.on;
2020
- if (walk) return {
2075
+ const fork = forked.find((one) => one.note === reply.note);
2076
+ if (fork) return fork.on ? {
2021
2077
  ...answer,
2022
- on: walk
2023
- };
2078
+ on: fork.on
2079
+ } : answer;
2080
+ const seen = lastSeen(review, reply.note, number);
2024
2081
  const flow = seen && flows.find((flow) => flow.name === seen.on.flow);
2025
2082
  const then = seen && review.rounds.find((round) => round.number === seen.round)?.flows.find((kept) => kept.name === seen.on.flow);
2026
- if (!seen || seen.on.app || !flow || !then) return answer;
2083
+ if (!seen || !flow || !then) return answer;
2027
2084
  const from = {
2028
2085
  flow: then,
2029
2086
  readings: await readingsOn(repo.reviews, review.id, seen.round, then)
@@ -2204,21 +2261,25 @@ async function wait(repo, id) {
2204
2261
  return ` ${turn.agent ? `${turn.by === you ? "You" : turn.by}, in round ${turn.round}` : turn.verdict ? `${turn.by}, ${turn.verdict}` : turn.by}${turn.words.trim() ? `: ${turn.words}` : ""}`;
2205
2262
  });
2206
2263
  const picture = pictures.get(note);
2207
- console.log(`\n ${note} ${where(notes.get(note)?.note.on)}${picture ? `\n As they saw it: ${picture}` : ""}\n${turns.join("\n")}`);
2264
+ const kept = notes.get(note);
2265
+ const from = kept?.note.on?.app && review.rounds.find((one) => one.number === kept.round)?.flows.find((flow) => flow.name === kept.note.on.app.from.flow);
2266
+ console.log(`\n ${note} ${where(kept?.note.on, from)}${picture ? `\n As they saw it: ${picture}` : ""}\n${turns.join("\n")}`);
2208
2267
  }
2209
- if (asking.some((note) => notes.get(note)?.note.on?.app)) console.log(`\nWhat they did in the app is walked again on the new version when you record the round, and record says whether it still gets to their note. playthrough walk ${id} does that while you work.`);
2268
+ if (asking.some((note) => notes.get(note)?.note.on?.app)) console.log(`\nWhat they did in the app is yours to write, as a fork of the flow they took over, in its file: test.fork("<what they do there>", { notes: ["<the note's id>"] }, async ({ page }) => { … }), with steps as a flow's. Record runs the flow to where they took it over, then the fork, every round until the note is settled, and puts your answer on what the note is on, where the fork first shows it. playthrough forks ${id} runs it while you work.`);
2210
2269
  const first = pictures.size ? "Look at what each note is on, change" : "Change";
2211
2270
  console.log(`\n${first} the app, then record round ${round}: playthrough record ${id}. Then answer each note in that round's round.json, under “replies”: record says where.`);
2212
2271
  return console.log(`New to this review? Run playthrough alone for how Playthrough works. Its flows are in ${repo.show(repo.flows(id))}.`);
2213
2272
  }
2214
2273
  }
2215
- function where(place) {
2274
+ function where(place, from) {
2216
2275
  if (!place) return "on the work in general";
2217
2276
  const at = place.at === void 0 ? "" : ` at ${time(place.at)}${place.until === void 0 ? "" : ` to ${time(place.until)}`}`;
2218
2277
  const on = place.element ? `, the ${place.element.role} “${place.element.name}”` : place.point ? `, ${share(place.point.x)} across and ${share(place.point.y)} down the screen` : place.box ? `, the part of the screen ${share(place.box.x)} across and ${share(place.box.y)} down` : "";
2219
2278
  if (place.app) {
2220
2279
  const did = place.app.did.length ? ` and carried on by hand (${list(place.app.did.map(done))})` : "";
2221
- return `on “${place.app.from.flow}”, in the app: they took the flow over at ${time(place.app.from.at)}${did}${on}`;
2280
+ const paused = from && pausedAt(from, place.app.from.at);
2281
+ const step = paused ? `, ${paused.actions ? `after ${paused.actions} of the actions in` : "at the end of"} its step “${paused.step}”` : "";
2282
+ return `on “${place.app.from.flow}”, in the app: they took the flow over at ${time(place.app.from.at)}${step}${did}${on}`;
2222
2283
  }
2223
2284
  return `on “${place.flow}”${at}${on}`;
2224
2285
  }
@@ -2238,7 +2299,7 @@ const takes = {
2238
2299
  start: [2, `playthrough start <review> "<title>" "<what was asked>"`],
2239
2300
  record: [0, "playthrough record <review>"],
2240
2301
  wait: [0, "playthrough wait <review>"],
2241
- walk: [0, "playthrough walk <review>"],
2302
+ forks: [0, "playthrough forks <review>"],
2242
2303
  open: [0, "playthrough open [<review>]"],
2243
2304
  "take-over": [2, `playthrough take-over <review> "<flow>" ["<step>"]`]
2244
2305
  };
@@ -2255,7 +2316,7 @@ try {
2255
2316
  else if (command === "start") await start(repo(), review, words[0], words[1]);
2256
2317
  else if (command === "record") await record(repo(), review);
2257
2318
  else if (command === "wait") await wait(repo(), review);
2258
- else if (command === "walk") await walk(repo(), review);
2319
+ else if (command === "forks") await forks(repo(), review);
2259
2320
  else if (command === "take-over") await takeOver$1(repo(), review, words[0], words[1]);
2260
2321
  }
2261
2322
  } catch (error) {
@@ -1,4 +1,4 @@
1
- import { f as waitsFor } from "./flows-vi0amwSo.js";
1
+ import { p as waitsFor } from "./flows-CbrwgoHB.js";
2
2
  import { availableParallelism } from "node:os";
3
3
  import { defineConfig } from "@playwright/test";
4
4
  import { fileURLToPath } from "node:url";
@@ -50,7 +50,11 @@ const Place = z.strictObject({
50
50
  }),
51
51
  did: z.array(Did).describe("What the person did there, in order"),
52
52
  recording: fileName("needs its recording: the name of a file in the round").describe("The flow up to where it was taken over, then the person's own steps"),
53
- length: z.number({ error: "needs its length, in seconds" }).positive({ error: "needs its length, in seconds" })
53
+ length: z.number({ error: "needs its length, in seconds" }).positive({ error: "needs its length, in seconds" }),
54
+ steps: z.array(z.strictObject({
55
+ title: text("needs its title"),
56
+ at: seconds$1("needs its start, in seconds")
57
+ })).optional().describe("Run again as the agent wrote it, a fork of its flow: its steps, each at its start")
54
58
  }).optional().describe("In the app: on the person's own recording, which branches off a flow where they took it over. Its flow names the branch")
55
59
  }).refine((place) => [
56
60
  place.element,
package/src/reporter.js CHANGED
@@ -1 +1 @@
1
- export { t as ACTIONS, n as ASSETS, r as DONE_ON, a as GLIMPSES, o as HANDS, s as HOLD, c as PERSON, l as PICTURE, u as READINGS, d as RECORDING, f as WROTE, i as default, p as firstLine, m as glimpsesOf, h as readingsOf, g as readingsOn, _ as seconds } from "./reporter-S9fOk0za.js";
1
+ export { t as ACTIONS, n as ASSETS, r as DONE_ON, a as GLIMPSES, o as HANDS, s as HOLD, c as PERSON, l as PICTURE, u as READINGS, d as RECORDING, f as WROTE, i as default, p as firstLine, m as glimpsesOf, h as readingsOf, g as readingsOn, _ as seconds } from "./reporter-DlS7qehW.js";