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.
- package/README.md +5 -5
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +368 -118
- package/src/board/compareStructure.d.ts +32 -0
- package/src/board/compareStructure.js +85 -0
- package/src/board/markdown.d.ts +15 -0
- package/src/board/markdown.js +177 -0
- package/src/board/project.d.ts +19 -1
- package/src/board/project.js +134 -2
- package/src/board/readWall.d.ts +10 -1
- package/src/board/readWall.js +26 -2
- package/src/board/reducer.d.ts +18 -1
- package/src/board/reducer.js +50 -4
package/src/board/reducer.js
CHANGED
|
@@ -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": {
|