plotcoder-board 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,4 +104,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
104
104
 
105
105
  ## Status
106
106
 
107
- Version 0.1.31. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
107
+ Version 0.1.33. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotcoder-board",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -1059,7 +1059,8 @@ function summarize(state) {
1059
1059
  // The board it pays off on, named here as read_wall names it (round fifteen, entry 44).
1060
1060
  const laterName = note.payoffBoardId ? (lastHeld?.project?.boards?.find((meta) => meta.id === note.payoffBoardId)?.name ?? note.payoffBoardId) : null;
1061
1061
  const laterAt = note.payoffBoardId && note.payoffNoteId && lastHeld?.project ? ` at ${episodeLabel(lastHeld.project, lastHeld.boards, note.payoffBoardId, note.payoffNoteId)} "${lastHeld.boards[note.payoffBoardId]?.notes?.find((item) => item.id === note.payoffNoteId)?.headline ?? note.payoffNoteId}"` : "";
1062
- const plant = note.plants ? (note.payoffBoardId ? `, plants → pays off later on "${laterName}"${laterAt || ", no scene there claimed yet"}` : ", plants") : "";
1062
+ const what = note.plantsWhat ? `: ${note.plantsWhat}` : "";
1063
+ const plant = note.plants ? (note.payoffBoardId ? `, plants${what} → pays off later on "${laterName}"${laterAt || ", no scene there claimed yet"}` : `, plants${what}`) : "";
1063
1064
  // The receiving end (R58): what this card pays off from another board, composed from the project.
1064
1065
  const pays = paidByHere.filter((item) => item.id === note.id).map((item) => `, pays off "${item.fromHeadline}" from "${item.fromBoardName}" (${episodeLabel(lastHeld.project, lastHeld.boards, item.fromBoardId, item.fromNoteId)})`).join("");
1065
1066
  const snap = state.revision?.snapshot?.[note.id];
@@ -1384,6 +1385,7 @@ server.registerTool(
1384
1385
  rank: args.rank,
1385
1386
  lengthEighths: args.pages === undefined ? undefined : toEighths(args.pages),
1386
1387
  plants: args.plants,
1388
+ plantsWhat: args.plantsWhat,
1387
1389
  location: args.location,
1388
1390
  when: args.when,
1389
1391
  open: args.open,
@@ -1428,7 +1430,7 @@ server.registerTool(
1428
1430
  result?.rank === "beat" ? "a beat" : "a scene",
1429
1431
  result?.lengthEighths === null ? "about a page (unsized: the writer's guess until set_length)" : `${formatPages(noteEighths(result))} ${formatPages(noteEighths(result)) === "1" ? "page" : "pages"}`,
1430
1432
  result?.color ? `${result.color} paper${args.color ? "" : once("paper", " (pass color to choose)")}` : null,
1431
- result?.plants ? "corner folded" : null,
1433
+ result?.plants ? `corner folded${result.plantsWhat ? ` — plants ${result.plantsWhat}` : ""}` : null,
1432
1434
  result?.location ? `at ${result.location}` : `no place yet${once("place", " (location here, or set_location)")}`,
1433
1435
  result?.when ? `when: ${result.when}` : null,
1434
1436
  result?.open ? `open: "${result.open}" (listed, not asked about)` : null,
@@ -2783,12 +2785,14 @@ server.registerTool(
2783
2785
  `Fold the corner of cards — mark them as planting something — or unfold them. ${wordSentence("corner")} The setup arrow is create_arrow with kind 'setup'. A fold that pays off in a later episode: pass later, another board of the project by name, id or number — a board that exists; new_board makes one — and, once you know it, at: the scene on that board that pays it off, by id or headline. A board alone is a promise: the reading lists the card under 'later' and, once that board holds cards, asks which scene until one claims it; with at, both boards' readings name the payoff and the paying-off card says so. later '' forgets the board; at '' keeps the board and forgets the scene. set_payoff makes the same claim from the other board. Folding never moves a card.`,
2784
2786
  inputSchema: {
2785
2787
  ids: z.array(z.string()).min(1),
2786
- plants: z.boolean(),
2788
+ plants: z.boolean().optional(),
2789
+ what: z.string().optional(),
2787
2790
  later: z.string().optional(),
2788
2791
  at: z.string().optional(),
2789
2792
  },
2790
2793
  },
2791
2794
  async (args) => {
2795
+ if (args.plants === undefined && args.what === undefined) return ok("Say which: plants (true folds, false unfolds), or what (the writer's words for what it plants, which folds the card; \"\" keeps the fold and drops the words).");
2792
2796
  // A series plant (R50): the fold pays off on another board of the project.
2793
2797
  // The kernel cannot check the board exists; this door can, before anything lands.
2794
2798
  let target = null;
@@ -2823,7 +2827,7 @@ server.registerTool(
2823
2827
  // What the folds claimed before, so the reply can say what a new claim replaced (round sixteen, entry 26).
2824
2828
  const before = new Map(current().notes.filter((note) => args.ids.includes(note.id)).map((note) => [note.id, { plants: note.plants, boardId: note.payoffBoardId, noteId: note.payoffNoteId }]));
2825
2829
  const alreadyFolded = args.plants && args.ids.every((id) => before.get(id)?.plants);
2826
- let { result } = step({ type: "set_plant", ids: args.ids, plants: args.plants });
2830
+ let { result } = step({ type: "set_plant", ids: args.ids, ...(args.plants !== undefined ? { plants: args.plants } : {}), ...(args.what !== undefined ? { what: args.what } : {}) });
2827
2831
  let laterLine = alreadyFolded ? " (already folded)" : "";
2828
2832
  if (forgetting) {
2829
2833
  const cleared = step({ type: "set_payoff_board", ids: args.ids, boardId: null });
@@ -2851,10 +2855,11 @@ server.registerTool(
2851
2855
  const { result, laterLine } = value;
2852
2856
  const count = result?.length ?? 0;
2853
2857
  if (!changed || count === 0) return ok("No change: those cards were already that way, or the ids are not on the board.");
2858
+ const what = result?.[0]?.plantsWhat ?? "";
2854
2859
  return ok(
2855
- args.plants
2856
- ? `${count} card(s) now plant something${where(live)}.${laterLine || " read_wall will ask about each until a setup arrow pays it off, or later names the board it pays off on."}`
2857
- : `${count} card(s) no longer marked as planting${where(live)}.`,
2860
+ args.plants !== false
2861
+ ? `${count} card(s) now plant ${what ? `"${what}"` : "something"}${where(live)}.${what ? ` The card says "Plants · ${what}", and read_wall asks where ${what} come${/s$/i.test(what) ? "" : "s"} back` : " read_wall will ask about each"}${laterLine || " until a setup arrow pays it off, or later names the board it pays off on."}${args.what === "" ? " The fold keeps no words now." : ""}`
2862
+ : `${count} card(s) no longer marked as planting${where(live)}.${result?.some((note) => !note.plantsWhat) && args.ids.length ? "" : ""}`,
2858
2863
  result,
2859
2864
  );
2860
2865
  },
@@ -3180,6 +3185,15 @@ function cardsByRef(state, refs) {
3180
3185
  return { found, missing };
3181
3186
  }
3182
3187
 
3188
+ /** What tying a thread at both ends did to the fold (R62), in words for the reply. */
3189
+ function foldLine(state, fold, thread) {
3190
+ if (!fold) return "";
3191
+ const head = (id) => `"${state.notes.find((note) => note.id === id)?.headline ?? id}"`;
3192
+ if (fold.kept) return ` ${head(fold.firstId)} is folded for ${fold.what}, so "${thread.name}" stays a thread and no arrow is drawn: a card has one fold.`;
3193
+ const did = [fold.folded ? `folded ${head(fold.firstId)}` : null, fold.named ? `named its fold "${thread.name}"` : null, fold.arrow ? `drew the setup arrow to ${head(fold.lastId)}` : null].filter(Boolean);
3194
+ return did.length ? ` Tied at both ends, so it is the fold's now: ${did.join(", ")}.` : "";
3195
+ }
3196
+
3183
3197
  function threadLine(state, thread) {
3184
3198
  const order = storyOrder(state).filter((note) => thread.noteIds.includes(note.id));
3185
3199
  const cards = order.length ? order.map((note) => `"${note.headline}"`).join(" → ") : "no card yet";
@@ -3208,7 +3222,7 @@ server.registerTool(
3208
3222
  if (!changed) return ok("No thread made: a thread needs a name.");
3209
3223
  const asks = [result.startOpen ? "where it is first seen" : null, result.endOpen ? "where it comes out" : null].filter(Boolean);
3210
3224
  return ok(
3211
- `Named the thread ${threadLine(state, result)}${where(live)}.${asks.length ? ` The reading asks ${asks.join(" and ")} until update_thread ties ${asks.length === 1 ? "that end" : "them"}.` : result.noteIds.length ? " Both ends are tied; the reading lists it and asks nothing." : " No card yet: the reading asks where it is first seen and where it comes out."} The wall draws it as a string through its cards, a loose end where one is open.`,
3225
+ `Named the thread ${threadLine(state, result)}${where(live)}.${asks.length ? ` The reading asks ${asks.join(" and ")} until update_thread ties ${asks.length === 1 ? "that end" : "them"}.` : result.noteIds.length ? " Both ends are tied; the reading lists it and asks nothing." : " No card yet: the reading asks where it is first seen and where it comes out."}${foldLine(state, result.fold, result)} The wall draws it as a string through its cards, a loose end where one is open.`,
3212
3226
  result,
3213
3227
  );
3214
3228
  },
@@ -3250,7 +3264,7 @@ server.registerTool(
3250
3264
  const tied = [result.before.startOpen && !result.thread.startOpen ? "its start" : null, result.before.endOpen && !result.thread.endOpen ? "its end" : null].filter(Boolean);
3251
3265
  const reopened = [!result.before.startOpen && result.thread.startOpen ? "its start" : null, !result.before.endOpen && result.thread.endOpen ? "its end" : null].filter(Boolean);
3252
3266
  return ok(
3253
- `Now ${threadLine(state, result.thread)}${where(live)}.${tied.length ? ` Tied ${tied.join(" and ")}; the reading stops asking about ${tied.length === 1 ? "it" : "them"}.` : ""}${reopened.length ? ` Opened ${reopened.join(" and ")}; the reading asks about ${reopened.length === 1 ? "it" : "them"} again.` : ""}`,
3267
+ `Now ${threadLine(state, result.thread)}${where(live)}.${tied.length ? ` Tied ${tied.join(" and ")}; the reading stops asking about ${tied.length === 1 ? "it" : "them"}.` : ""}${reopened.length ? ` Opened ${reopened.join(" and ")}; the reading asks about ${reopened.length === 1 ? "it" : "them"} again.` : ""}${foldLine(state, result.fold, result.thread)}`,
3254
3268
  result.thread,
3255
3269
  );
3256
3270
  },
@@ -122,7 +122,7 @@ export function toFountain(state, options = {}) {
122
122
  const revisionMarksFor = (_list, card) => Boolean(revisionOf.get(card.id)?.revised);
123
123
  const cast = (note.characterIds ?? []).map((id) => nameOf.get(id)).filter(Boolean);
124
124
  if (cast.length) marks.push(`with ${cast.join(", ")}`);
125
- if (note.plants) marks.push("plants something to pay off later");
125
+ if (note.plants) marks.push(note.plantsWhat ? `plants ${note.plantsWhat}` : "plants something to pay off later");
126
126
  if (note.open) marks.push(`open: ${note.open}`);
127
127
  if (note.whenOpen) marks.push(`when open: ${note.whenOpen}`);
128
128
  const onThreads = (state.threads ?? []).filter((thread) => thread.noteIds.includes(note.id)).map((thread) => thread.name);
@@ -420,7 +420,7 @@ export function readWall(state, options = {}) {
420
420
  findings.push({
421
421
  kind: "unpaid",
422
422
  ids: [note.id],
423
- text: `${quote(note)} plants something, and no arrow pays it off. Where does it come back?`,
423
+ text: `${quote(note)} plants ${note.plantsWhat || "something"}, and no arrow pays it off. Where does ${note.plantsWhat ? "it" : "it"} come back?`.replace("Where does it come back?", note.plantsWhat ? `Where ${/s$/i.test(note.plantsWhat) ? "do" : "does"} ${note.plantsWhat} come back?` : "Where does it come back?"),
424
424
  });
425
425
  }
426
426
  }
@@ -614,7 +614,8 @@ export function describeSetups(reading, state) {
614
614
  : setup.eighths === 0
615
615
  ? "in the same place on the wall"
616
616
  : `about ${pages(-setup.eighths)} pages earlier`;
617
- return `"${name(setup.from)}" sets up "${name(setup.to)}", ${distance}`;
617
+ const what = byId.get(setup.from)?.plantsWhat ?? "";
618
+ return `"${name(setup.from)}" sets up "${name(setup.to)}"${what ? ` — ${what}` : ""}, ${distance}`;
618
619
  });
619
620
  }
620
621
 
@@ -79,6 +79,8 @@ export type BoardNote = {
79
79
  characterIds: string[];
80
80
  /** The corner is folded: this card plants something that must pay off (R31). */
81
81
  plants: boolean;
82
+ /** What the fold plants, in the writer's words (R62), or empty; never set on an unfolded card. */
83
+ plantsWhat: string;
82
84
  /** When folded: the id of another board of the project where it pays off (R50), or null. */
83
85
  payoffBoardId: string | null;
84
86
  /** The scene on that board that pays it off (R58), or null while the board is only a promise. */
@@ -179,6 +181,8 @@ export type Command =
179
181
  lengthEighths?: number;
180
182
  characterIds?: string[];
181
183
  plants?: boolean;
184
+ /** What it plants, in the writer's words (R62); naming a plant folds the card. */
185
+ plantsWhat?: string;
182
186
  location?: string;
183
187
  when?: string;
184
188
  whenOpen?: string;
@@ -209,7 +213,7 @@ export type Command =
209
213
  | { type: "remove_character"; id: string }
210
214
  | ({ type: "update_character"; id: string } & Partial<Record<CharacterField, string>>)
211
215
  | { type: "set_cast"; ids: string[]; characterIds: string[] }
212
- | { type: "set_plant"; ids: string[]; plants: boolean }
216
+ | { type: "set_plant"; ids: string[]; plants?: boolean; what?: string }
213
217
  | { type: "set_open"; ids: string[]; open: string }
214
218
  | { type: "set_payoff_board"; ids: string[]; boardId: string | null; noteId?: string | null }
215
219
  | { type: "set_location"; ids: string[]; location: string }
@@ -252,6 +252,8 @@ export function seedState(now = nowIso()) {
252
252
  whenOpen: "",
253
253
  text: "",
254
254
  plants: false,
255
+ // What the fold plants, in the writer's words (R62), or nothing.
256
+ plantsWhat: "",
255
257
  // A fold that pays off on another board — a later episode — names it here;
256
258
  // null claims nothing (R50). The scene there that pays it off, once one
257
259
  // does (R58); null while the board is a promise.
@@ -356,6 +358,8 @@ export function normalizeState(value) {
356
358
  const characterIds = knownCast(note?.characterIds, characters);
357
359
  // Cards written before R31 have no fold; a plant is a claim you make.
358
360
  const plants = note?.plants === true;
361
+ // Cards folded before R62 say "something": the fold's words are the writer's, or nothing.
362
+ const plantsWhat = plants && typeof note?.plantsWhat === "string" ? note.plantsWhat : "";
359
363
  // Cards written before R50 pay off on their own board or not at all.
360
364
  const payoffBoardId = plants && typeof note?.payoffBoardId === "string" && note.payoffBoardId ? note.payoffBoardId : null;
361
365
  // Cards written before R58 name a board and no scene on it.
@@ -377,6 +381,7 @@ export function normalizeState(value) {
377
381
  Array.isArray(note.characterIds) &&
378
382
  sameIds(note.characterIds, characterIds) &&
379
383
  note.plants === plants &&
384
+ note.plantsWhat === plantsWhat &&
380
385
  note.payoffBoardId === payoffBoardId &&
381
386
  note.payoffNoteId === payoffNoteId &&
382
387
  note.open === open &&
@@ -388,7 +393,7 @@ export function normalizeState(value) {
388
393
  return note;
389
394
  }
390
395
  patched = true;
391
- return { ...note, rank, lengthEighths, characterIds, plants, payoffBoardId, payoffNoteId, open, location, when, whenOpen, text };
396
+ return { ...note, rank, lengthEighths, characterIds, plants, plantsWhat, payoffBoardId, payoffNoteId, open, location, when, whenOpen, text };
392
397
  });
393
398
 
394
399
  // Boards written before the production half (Roadmap 2, item 8) have no
@@ -469,6 +474,48 @@ function cleanOpen(value) {
469
474
  return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
470
475
  }
471
476
 
477
+ /**
478
+ * The combine log's rule, both halves (R60, R62). A thread tied at both ends
479
+ * through two or more cards is a plant between two scenes that exist, and
480
+ * that is the fold's and the setup arrow's to draw: when the first card's
481
+ * fold is free — unfolded, or folded for the same thing, or folded with no
482
+ * words — the card is folded, the fold takes the thread's name, and a setup
483
+ * arrow runs from the first card to the last. When the first card's fold is
484
+ * another thing's, the thread stays a thread and nothing is drawn. Returns
485
+ * the state and what it did, so the door can say it.
486
+ */
487
+ function tieIntoFold(state, thread, now) {
488
+ const none = { state, fold: null };
489
+ if (thread.startOpen || thread.endOpen || thread.noteIds.length < 2) return none;
490
+ const firstId = thread.noteIds[0];
491
+ const lastId = thread.noteIds[thread.noteIds.length - 1];
492
+ const first = state.notes.find((note) => note.id === firstId);
493
+ if (!first) return none;
494
+ const what = first.plantsWhat ?? "";
495
+ if (first.plants && what && what.toLowerCase() !== thread.name.toLowerCase()) {
496
+ return { state, fold: { kept: true, firstId, what } };
497
+ }
498
+ let next = state;
499
+ const folded = !first.plants;
500
+ const named = !what;
501
+ if (folded || named) {
502
+ next = {
503
+ ...next,
504
+ notes: next.notes.map((note) => (note.id === firstId ? bump(note, { plants: true, plantsWhat: thread.name }, now) : note)),
505
+ };
506
+ }
507
+ const hasArrow = next.arrows.some((arrow) => arrow.kind === "setup" && arrow.from === firstId && arrow.to === lastId);
508
+ let arrow = null;
509
+ if (!hasArrow) {
510
+ const drawn = applyCommand(next, { type: "create_arrow", from: firstId, to: lastId, kind: "setup" }, now);
511
+ if (drawn.changed) {
512
+ next = drawn.state;
513
+ arrow = { from: firstId, to: lastId };
514
+ }
515
+ }
516
+ return { state: next, fold: { kept: false, firstId, lastId, folded, named, arrow } };
517
+ }
518
+
472
519
  /** A thread's name as the writer typed it, one line, spaces collapsed (R60). */
473
520
  function cleanThreadName(value) {
474
521
  return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
@@ -512,7 +559,9 @@ export function applyCommand(state, command, now = nowIso()) {
512
559
  ? null
513
560
  : clampEighths(command.lengthEighths, DEFAULT_NOTE_EIGHTHS, MAX_NOTE_EIGHTHS),
514
561
  characterIds: knownCast(command.characterIds, state.characters ?? []),
515
- plants: command.plants === true,
562
+ plants: command.plants === true || Boolean(cleanOpen(command.plantsWhat)),
563
+ // What it plants, in the writer's words (R62): naming a plant folds the card.
564
+ plantsWhat: command.plants === false ? "" : cleanOpen(command.plantsWhat),
516
565
  payoffBoardId: null,
517
566
  payoffNoteId: null,
518
567
  open: cleanOpen(command.open),
@@ -768,7 +817,8 @@ export function applyCommand(state, command, now = nowIso()) {
768
817
  startOpen: command.startOpen === true,
769
818
  endOpen: command.endOpen === true,
770
819
  };
771
- return { state: { ...state, threads: [...(state.threads ?? []), thread] }, changed: true, result: thread };
820
+ const tied = tieIntoFold({ ...state, threads: [...(state.threads ?? []), thread] }, thread, now);
821
+ return { state: tied.state, changed: true, result: { ...thread, fold: tied.fold } };
772
822
  }
773
823
 
774
824
  case "update_thread": {
@@ -785,10 +835,17 @@ export function applyCommand(state, command, now = nowIso()) {
785
835
  return { state, changed: false };
786
836
  }
787
837
  const next = { ...current, name, noteIds, startOpen, endOpen };
838
+ // A fold this thread named follows a rename (R62), so the two never drift apart on the wall's word alone.
839
+ let notes = state.notes;
840
+ if (name !== current.name && noteIds.length) {
841
+ const firstId = noteIds[0];
842
+ notes = notes.map((note) => (note.id === firstId && note.plants && (note.plantsWhat ?? "").toLowerCase() === current.name.toLowerCase() ? bump(note, { plantsWhat: name }, now) : note));
843
+ }
844
+ const tied = tieIntoFold({ ...state, notes, threads: state.threads.map((thread) => (thread.id === command.id ? next : thread)) }, next, now);
788
845
  return {
789
- state: { ...state, threads: state.threads.map((thread) => (thread.id === command.id ? next : thread)) },
846
+ state: tied.state,
790
847
  changed: true,
791
- result: { thread: next, before: current },
848
+ result: { thread: next, before: current, fold: tied.fold },
792
849
  };
793
850
  }
794
851
 
@@ -1027,6 +1084,7 @@ export function applyCommand(state, command, now = nowIso()) {
1027
1084
  lengthEighths: null,
1028
1085
  characterIds: [],
1029
1086
  plants: false,
1087
+ plantsWhat: "",
1030
1088
  payoffBoardId: null,
1031
1089
  payoffNoteId: null,
1032
1090
  open: "",
@@ -1183,15 +1241,20 @@ export function applyCommand(state, command, now = nowIso()) {
1183
1241
  case "set_plant": {
1184
1242
  const ids = new Set(command.ids);
1185
1243
  if (ids.size === 0) return { state, changed: false };
1186
- const plants = command.plants === true;
1244
+ // What the fold plants, in the writer's words (R62): `what` names it and
1245
+ // folds an unfolded card; `plants` alone keeps the words; unfolding
1246
+ // forgets them, as it forgets where it paid off.
1247
+ const what = typeof command.what === "string" ? cleanOpen(command.what) : null;
1187
1248
  const touched = [];
1188
1249
  const notes = state.notes.map((note) => {
1189
1250
  if (!ids.has(note.id)) return note;
1251
+ const plants = typeof command.plants === "boolean" ? command.plants : note.plants || Boolean(what);
1252
+ const plantsWhat = !plants ? "" : what === null ? (note.plantsWhat ?? "") : what;
1190
1253
  // Unfolding forgets where it paid off; a claim that no longer stands.
1191
1254
  const payoffBoardId = plants ? note.payoffBoardId : null;
1192
1255
  const payoffNoteId = plants ? (note.payoffNoteId ?? null) : null;
1193
- if (note.plants === plants && note.payoffBoardId === payoffBoardId && note.payoffNoteId === payoffNoteId) return note;
1194
- const next = bump(note, { plants, payoffBoardId, payoffNoteId }, now);
1256
+ if (note.plants === plants && (note.plantsWhat ?? "") === plantsWhat && note.payoffBoardId === payoffBoardId && note.payoffNoteId === payoffNoteId) return note;
1257
+ const next = bump(note, { plants, plantsWhat, payoffBoardId, payoffNoteId }, now);
1195
1258
  touched.push(next);
1196
1259
  return next;
1197
1260
  });
@@ -77,7 +77,7 @@ export const WORD_GROUPS = [
77
77
  id: "corner",
78
78
  name: "The folded corner",
79
79
  sentence:
80
- "This card plants something — a gun on the wall — that must pay off later. The card says unpaid, and the bar’s Asks line keeps asking, until a setup arrow leaves it. One thing, three words: the corner is folded, the tool’s flag is plants, the wall’s question is unpaid.",
80
+ "This card plants something — a gun on the wall — that must pay off later, and can say what, in the writer’s words: Plants · the wrong tools. The card says unpaid, and the bar’s Asks line keeps asking where it comes back, until a setup arrow leaves it. One thing, three words: the corner is folded, the tool’s flag is plants, the wall’s question is unpaid.",
81
81
  target: "corner",
82
82
  },
83
83
  {