plotcoder-board 0.1.13 → 0.1.15

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.
@@ -33,7 +33,7 @@ import { lockFrom, REVISION_COLORS } from "./numbering.js";
33
33
  // same person on every card. Long term the record grows — what they look like,
34
34
  // the details a writer needs to pull up — which is why it has an id and
35
35
  // timestamps now rather than being a word on a card.
36
- function isCharacter(value) {
36
+ export function isCharacter(value) {
37
37
  return Boolean(value) && typeof value.id === "string" && typeof value.name === "string";
38
38
  }
39
39
 
@@ -45,7 +45,7 @@ function isCharacter(value) {
45
45
  export const CHARACTER_FIELDS = ["looks", "voice", "wants", "needs", "notes"];
46
46
 
47
47
  /** A roster record with every page field present, so the page never reads undefined. */
48
- function fillCharacter(character) {
48
+ export function fillCharacter(character) {
49
49
  let filled = character;
50
50
  for (const field of CHARACTER_FIELDS) {
51
51
  if (typeof filled[field] !== "string") {
@@ -93,7 +93,7 @@ export function atPlace(note, place) {
93
93
  return Boolean(note.location) && samePlace(note.location, place);
94
94
  }
95
95
 
96
- function sameName(a, b) {
96
+ export function sameName(a, b) {
97
97
  return a.trim().toLowerCase() === b.trim().toLowerCase();
98
98
  }
99
99
 
@@ -101,6 +101,19 @@ function sameIds(a, b) {
101
101
  return a.length === b.length && a.every((id, index) => id === b[index]);
102
102
  }
103
103
 
104
+ /** A question the writer has left (R53): its kind, the cards it was about, and the words it had. */
105
+ function isLeftQuestion(value) {
106
+ return (
107
+ !!value &&
108
+ typeof value === "object" &&
109
+ typeof value.kind === "string" &&
110
+ Array.isArray(value.ids) &&
111
+ value.ids.every((id) => typeof id === "string") &&
112
+ typeof value.text === "string" &&
113
+ typeof value.since === "string"
114
+ );
115
+ }
116
+
104
117
  /** Keep only ids that name someone in the roster, once each, in the order given. */
105
118
  function knownCast(ids, characters) {
106
119
  if (!Array.isArray(ids)) return [];
@@ -213,6 +226,7 @@ export function emptyState() {
213
226
  arrows: [],
214
227
  lock: null,
215
228
  revision: null,
229
+ left: [],
216
230
  };
217
231
  }
218
232
 
@@ -259,6 +273,7 @@ export function seedState(now = nowIso()) {
259
273
  arrows: [],
260
274
  lock: null,
261
275
  revision: null,
276
+ left: [],
262
277
  };
263
278
  }
264
279
 
@@ -355,6 +370,10 @@ export function normalizeState(value) {
355
370
  // lock and no revision; both are null until a draft goes out.
356
371
  const lock = value.lock && typeof value.lock === "object" && value.lock.numbers ? value.lock : null;
357
372
  const revision = value.revision && typeof value.revision === "object" && typeof value.revision.name === "string" ? value.revision : null;
373
+ // Boards written before R53 have no left questions: nothing is left until
374
+ // the writer leaves it.
375
+ const left = Array.isArray(value.left) ? value.left.filter(isLeftQuestion) : [];
376
+ const leftPatched = !Array.isArray(value.left) || left.length !== value.left.length;
358
377
  if (
359
378
  value.logline === logline &&
360
379
  value.targetEighths === targetEighths &&
@@ -362,7 +381,8 @@ export function normalizeState(value) {
362
381
  !arrowsPatched &&
363
382
  !patched &&
364
383
  value.lock === lock &&
365
- value.revision === revision
384
+ value.revision === revision &&
385
+ !leftPatched
366
386
  ) {
367
387
  return value;
368
388
  }
@@ -375,6 +395,7 @@ export function normalizeState(value) {
375
395
  arrows: arrowsPatched ? arrows : value.arrows,
376
396
  lock,
377
397
  revision,
398
+ left,
378
399
  };
379
400
  }
380
401
 
@@ -879,6 +900,31 @@ export function applyCommand(state, command, now = nowIso()) {
879
900
  return { state: { ...state, revision: null }, changed: true, result: null };
880
901
  }
881
902
 
903
+ // Leaving a question (R53): the writer's word on a question the wall
904
+ // asks, written on the wall as the question's kind, the cards it was
905
+ // about and the words it had. The reading holds it back while a question
906
+ // with those words is still what the wall would ask, and asks again on
907
+ // its own the moment the question would read differently. Never a
908
+ // dismissal: the kernel records the word; the reading decides.
909
+ case "leave_question": {
910
+ const kind = typeof command.kind === "string" ? command.kind : "";
911
+ const ids = Array.isArray(command.ids) ? command.ids.filter((id) => typeof id === "string") : [];
912
+ const text = typeof command.text === "string" ? command.text : "";
913
+ if (!kind || !text) return { state, changed: false };
914
+ const entry = { kind, ids, text, since: now };
915
+ const rest = (state.left ?? []).filter((item) => !(item.kind === kind && sameIds(item.ids, ids)));
916
+ return { state: { ...state, left: [...rest, entry] }, changed: true, result: entry };
917
+ }
918
+
919
+ case "ask_again": {
920
+ const kind = typeof command.kind === "string" ? command.kind : "";
921
+ const ids = Array.isArray(command.ids) ? command.ids : null;
922
+ const gone = (state.left ?? []).filter((item) => item.kind === kind && (!ids || sameIds(item.ids, ids)));
923
+ if (gone.length === 0) return { state, changed: false };
924
+ const left = (state.left ?? []).filter((item) => !gone.includes(item));
925
+ return { state: { ...state, left }, changed: true, result: gone };
926
+ }
927
+
882
928
  // Where a scene happens (R37): one place on one or more cards; an empty
883
929
  // place clears it.
884
930
  case "set_location": {