plotcoder-board 0.1.17 → 0.1.19
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 +7 -4
- package/package.json +2 -1
- package/scripts/plotcoder-mcp-server.mjs +254 -84
- package/src/board/agents.js +4 -4
- package/src/board/fdx.js +29 -16
- package/src/board/fountain.d.ts +2 -0
- package/src/board/fountain.js +24 -6
- package/src/board/markdown.d.ts +2 -1
- package/src/board/markdown.js +32 -14
- package/src/board/numbering.d.ts +8 -1
- package/src/board/numbering.js +36 -3
- package/src/board/organize.js +3 -30
- package/src/board/readWall.d.ts +3 -1
- package/src/board/readWall.js +69 -9
- package/src/board/reducer.d.ts +8 -2
- package/src/board/reducer.js +52 -7
- package/src/board/words.js +1 -1
- package/src/board/workflows.js +1 -1
package/src/board/reducer.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export type BoardNote = {
|
|
|
83
83
|
payoffBoardId: string | null;
|
|
84
84
|
/** Where the scene happens (R37): a phrase in the writer's words; empty until set. */
|
|
85
85
|
location: string;
|
|
86
|
+
/** When the scene happens, as the writer says it — "night", "day four, dawn" — printed after the place on the heading (R55). Empty when unsaid. */
|
|
87
|
+
when: string;
|
|
86
88
|
/** The scene's text in Fountain (R23 b): action, cues, dialogue; empty until written. */
|
|
87
89
|
text: string;
|
|
88
90
|
createdAt: string;
|
|
@@ -128,6 +130,8 @@ export type LeftQuestion = {
|
|
|
128
130
|
/** The question's words when it was left; it comes back when they would differ. */
|
|
129
131
|
text: string;
|
|
130
132
|
since: string;
|
|
133
|
+
/** The writer's reason, when they gave one. */
|
|
134
|
+
why?: string;
|
|
131
135
|
};
|
|
132
136
|
|
|
133
137
|
export type Pose = { id: string; x: number; y: number; rotate: number };
|
|
@@ -155,9 +159,10 @@ export type Command =
|
|
|
155
159
|
characterIds?: string[];
|
|
156
160
|
plants?: boolean;
|
|
157
161
|
location?: string;
|
|
162
|
+
when?: string;
|
|
158
163
|
text?: string;
|
|
159
164
|
}
|
|
160
|
-
| { type: "update_note"; id: string; headline?: string; change?: string; location?: string }
|
|
165
|
+
| { type: "update_note"; id: string; headline?: string; change?: string; location?: string; when?: string }
|
|
161
166
|
| { type: "move_note"; id: string; x: number; y: number }
|
|
162
167
|
| { type: "nudge_notes"; ids: string[]; dx: number; dy: number }
|
|
163
168
|
| { type: "recolor_notes"; ids: string[]; color: NoteColor }
|
|
@@ -181,13 +186,14 @@ export type Command =
|
|
|
181
186
|
| { type: "set_plant"; ids: string[]; plants: boolean }
|
|
182
187
|
| { type: "set_payoff_board"; ids: string[]; boardId: string | null }
|
|
183
188
|
| { type: "set_location"; ids: string[]; location: string }
|
|
189
|
+
| { type: "set_when"; ids: string[]; when: string }
|
|
184
190
|
| { type: "apply_template"; template: string; beats?: Array<{ name: string; prompt: string; at: number }> }
|
|
185
191
|
| { type: "set_text"; id: string; text: string }
|
|
186
192
|
| { type: "lock_numbers"; order?: string[] }
|
|
187
193
|
| { type: "unlock_numbers" }
|
|
188
194
|
| { type: "start_revision"; name: string; color?: string }
|
|
189
195
|
| { type: "end_revision" }
|
|
190
|
-
| { type: "leave_question"; kind: string; ids: string[]; text: string }
|
|
196
|
+
| { type: "leave_question"; kind: string; ids: string[]; text: string; why?: string }
|
|
191
197
|
| { type: "ask_again"; kind: string; ids?: string[] };
|
|
192
198
|
|
|
193
199
|
export type CommandResult = {
|
package/src/board/reducer.js
CHANGED
|
@@ -245,6 +245,7 @@ export function seedState(now = nowIso()) {
|
|
|
245
245
|
lengthEighths: null,
|
|
246
246
|
characterIds,
|
|
247
247
|
location: "",
|
|
248
|
+
when: "",
|
|
248
249
|
text: "",
|
|
249
250
|
plants: false,
|
|
250
251
|
// A fold that pays off on another board — a later episode — names it here;
|
|
@@ -347,6 +348,8 @@ export function normalizeState(value) {
|
|
|
347
348
|
const payoffBoardId = plants && typeof note?.payoffBoardId === "string" && note.payoffBoardId ? note.payoffBoardId : null;
|
|
348
349
|
// Cards written before R37 have no place; a scene is nowhere until it is.
|
|
349
350
|
const location = typeof note?.location === "string" ? note.location : "";
|
|
351
|
+
// Cards written before R55 have no when; a scene is at no time until it is.
|
|
352
|
+
const when = typeof note?.when === "string" ? note.when : "";
|
|
350
353
|
// Cards written before pages (R23 b) have no text; a scene is unwritten until it is.
|
|
351
354
|
const text = typeof note?.text === "string" ? note.text : "";
|
|
352
355
|
if (
|
|
@@ -358,12 +361,13 @@ export function normalizeState(value) {
|
|
|
358
361
|
note.plants === plants &&
|
|
359
362
|
note.payoffBoardId === payoffBoardId &&
|
|
360
363
|
note.location === location &&
|
|
364
|
+
note.when === when &&
|
|
361
365
|
note.text === text
|
|
362
366
|
) {
|
|
363
367
|
return note;
|
|
364
368
|
}
|
|
365
369
|
patched = true;
|
|
366
|
-
return { ...note, rank, lengthEighths, characterIds, plants, payoffBoardId, location, text };
|
|
370
|
+
return { ...note, rank, lengthEighths, characterIds, plants, payoffBoardId, location, when, text };
|
|
367
371
|
});
|
|
368
372
|
|
|
369
373
|
// Boards written before the production half (Roadmap 2, item 8) have no
|
|
@@ -414,6 +418,11 @@ function bump(note, patch, now) {
|
|
|
414
418
|
return { ...note, ...patch, updatedAt: now };
|
|
415
419
|
}
|
|
416
420
|
|
|
421
|
+
/** A when as the writer typed it, one line, spaces collapsed. */
|
|
422
|
+
function cleanWhen(value) {
|
|
423
|
+
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
424
|
+
}
|
|
425
|
+
|
|
417
426
|
function pruneGroups(groups) {
|
|
418
427
|
return groups.filter((group) => group.noteIds.length >= 2);
|
|
419
428
|
}
|
|
@@ -445,6 +454,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
445
454
|
plants: command.plants === true,
|
|
446
455
|
payoffBoardId: null,
|
|
447
456
|
location: cleanPlace(command.location),
|
|
457
|
+
when: cleanWhen(command.when),
|
|
448
458
|
text: typeof command.text === "string" ? command.text : "",
|
|
449
459
|
z: maxZ(state.notes) + 1,
|
|
450
460
|
createdAt: now,
|
|
@@ -465,6 +475,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
465
475
|
if (command.headline !== undefined) patch.headline = command.headline;
|
|
466
476
|
if (command.change !== undefined) patch.change = command.change;
|
|
467
477
|
if (command.location !== undefined) patch.location = cleanPlace(command.location);
|
|
478
|
+
if (command.when !== undefined) patch.when = cleanWhen(command.when);
|
|
468
479
|
updated = bump(note, patch, now);
|
|
469
480
|
return updated;
|
|
470
481
|
});
|
|
@@ -581,7 +592,17 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
581
592
|
const headlineOf = (id) => state.notes.find((note) => note.id === id)?.headline ?? id;
|
|
582
593
|
const notes = state.notes.filter((note) => note.id !== command.id);
|
|
583
594
|
const taken = state.arrows.filter((arrow) => arrow.from === command.id || arrow.to === command.id);
|
|
584
|
-
|
|
595
|
+
let arrows = state.arrows.filter((arrow) => !taken.includes(arrow));
|
|
596
|
+
// A card wired into a chain — one follows in, one follows out — leaves
|
|
597
|
+
// the chain joined behind it, as move_scene does; undo takes the join
|
|
598
|
+
// back with the card (round fourteen, entry 20).
|
|
599
|
+
const ins = taken.filter((arrow) => arrow.kind !== "setup" && arrow.to === command.id);
|
|
600
|
+
const outs = taken.filter((arrow) => arrow.kind !== "setup" && arrow.from === command.id);
|
|
601
|
+
let joined = null;
|
|
602
|
+
if (ins.length === 1 && outs.length === 1 && ins[0].from !== outs[0].to && !arrows.some((arrow) => arrow.from === ins[0].from && arrow.to === outs[0].to)) {
|
|
603
|
+
joined = { id: newId(), from: ins[0].from, to: outs[0].to, kind: "follows" };
|
|
604
|
+
arrows = [...arrows, joined];
|
|
605
|
+
}
|
|
585
606
|
const left = [];
|
|
586
607
|
const groups = pruneGroups(
|
|
587
608
|
state.groups.map((group) => {
|
|
@@ -600,6 +621,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
600
621
|
id: command.id,
|
|
601
622
|
headline: gone.headline,
|
|
602
623
|
arrows: taken.map((arrow) => ({ ...arrow, fromHeadline: headlineOf(arrow.from), toHeadline: headlineOf(arrow.to) })),
|
|
624
|
+
joined: joined ? { ...joined, fromHeadline: headlineOf(joined.from), toHeadline: headlineOf(joined.to) } : null,
|
|
603
625
|
groups: left,
|
|
604
626
|
},
|
|
605
627
|
};
|
|
@@ -608,13 +630,17 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
608
630
|
case "apply_poses": {
|
|
609
631
|
const byId = new Map(command.poses.map((pose) => [pose.id, pose]));
|
|
610
632
|
if (byId.size === 0) return { state, changed: false };
|
|
633
|
+
// Only a card that actually moves is touched, so a tidy that leaves a
|
|
634
|
+
// card where it was does not stamp it (round fourteen, entry 43).
|
|
635
|
+
let moved = 0;
|
|
611
636
|
const notes = state.notes.map((note) => {
|
|
612
637
|
const pose = byId.get(note.id);
|
|
613
|
-
return
|
|
614
|
-
|
|
615
|
-
|
|
638
|
+
if (!pose || (note.x === pose.x && note.y === pose.y && note.rotate === pose.rotate)) return note;
|
|
639
|
+
moved += 1;
|
|
640
|
+
return bump(note, { x: pose.x, y: pose.y, rotate: pose.rotate }, now);
|
|
616
641
|
});
|
|
617
|
-
|
|
642
|
+
if (moved === 0) return { state, changed: false };
|
|
643
|
+
return { state: { ...state, notes }, changed: true, result: { moved } };
|
|
618
644
|
}
|
|
619
645
|
|
|
620
646
|
case "settle_note": {
|
|
@@ -880,6 +906,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
880
906
|
plants: false,
|
|
881
907
|
payoffBoardId: null,
|
|
882
908
|
location: "",
|
|
909
|
+
when: "",
|
|
883
910
|
text: "",
|
|
884
911
|
createdAt: now,
|
|
885
912
|
updatedAt: now,
|
|
@@ -949,7 +976,8 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
949
976
|
const ids = Array.isArray(command.ids) ? command.ids.filter((id) => typeof id === "string") : [];
|
|
950
977
|
const text = typeof command.text === "string" ? command.text : "";
|
|
951
978
|
if (!kind || !text) return { state, changed: false };
|
|
952
|
-
const
|
|
979
|
+
const why = typeof command.why === "string" ? command.why.trim() : "";
|
|
980
|
+
const entry = why ? { kind, ids, text, since: now, why } : { kind, ids, text, since: now };
|
|
953
981
|
const rest = (state.left ?? []).filter((item) => !(item.kind === kind && sameIds(item.ids, ids)));
|
|
954
982
|
return { state: { ...state, left: [...rest, entry] }, changed: true, result: entry };
|
|
955
983
|
}
|
|
@@ -980,6 +1008,23 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
980
1008
|
return { state: { ...state, notes }, changed: true, result: touched };
|
|
981
1009
|
}
|
|
982
1010
|
|
|
1011
|
+
// When a scene happens (R55): "night", "day four, dawn" — the writer's
|
|
1012
|
+
// phrase, printed after the place on the scene heading; empty clears it.
|
|
1013
|
+
case "set_when": {
|
|
1014
|
+
const ids = new Set(command.ids);
|
|
1015
|
+
if (ids.size === 0) return { state, changed: false };
|
|
1016
|
+
const when = cleanWhen(command.when);
|
|
1017
|
+
const touched = [];
|
|
1018
|
+
const notes = state.notes.map((note) => {
|
|
1019
|
+
if (!ids.has(note.id) || note.when === when) return note;
|
|
1020
|
+
const next = bump(note, { when }, now);
|
|
1021
|
+
touched.push(next);
|
|
1022
|
+
return next;
|
|
1023
|
+
});
|
|
1024
|
+
if (touched.length === 0) return { state, changed: false };
|
|
1025
|
+
return { state: { ...state, notes }, changed: true, result: touched };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
983
1028
|
// Fold the corner (R31): this card plants something. A claim about the
|
|
984
1029
|
// card, like rank, so it never moves it and never touches its arrows.
|
|
985
1030
|
case "set_plant": {
|
package/src/board/words.js
CHANGED
|
@@ -165,7 +165,7 @@ export const WORD_GROUPS = [
|
|
|
165
165
|
{
|
|
166
166
|
id: "revisions",
|
|
167
167
|
name: "Locked numbers, revisions",
|
|
168
|
-
sentence: "Once a draft goes out, scene numbers stop moving and changes print in a colour; a scene added after the lock
|
|
168
|
+
sentence: "Once a draft goes out, scene numbers stop moving and changes print in a colour with a star, on the page and in every export; a scene added after the lock has a letter, 2A after 2, that is its place between locked scenes and follows it if it moves. For after the writing.",
|
|
169
169
|
},
|
|
170
170
|
],
|
|
171
171
|
},
|
package/src/board/workflows.js
CHANGED
|
@@ -28,7 +28,7 @@ export const WORKFLOWS = [
|
|
|
28
28
|
{ question: "Which scenes are the turns?", hint: "Name them, or say \"propose them and I will strike\".", tool: "set_rank" },
|
|
29
29
|
{ question: "Does it have acts?", hint: "If so, where does each break fall?", tool: "create_group" },
|
|
30
30
|
{ question: "Where does each scene happen?", hint: "In your own words. A scene that moves through one location is still one place.", tool: "set_location" },
|
|
31
|
-
{ question: "When does a scene happen, where that matters?", hint: "That night; the fourth of October. It goes in the headline
|
|
31
|
+
{ question: "When does a scene happen, where that matters?", hint: "That night; the fourth of October. It goes beside the place, never in the headline.", tool: "set_when" },
|
|
32
32
|
{ question: "Who is in each scene, and what do we call them?", hint: "A full name, or a role for someone unnamed — the man in 42. And who is only spoken of, never in a scene? They go in someone's notes, not the cast.", tool: "add_character, cast, update_character" },
|
|
33
33
|
{ question: "What is planted, and where does it pay off?", hint: "Name the episode when it pays off outside this one, so the fold is deliberate and the wall knows where to look.", tool: "set_plant with later, create_arrow" },
|
|
34
34
|
{ question: "Which scenes do you already know run long or short?", hint: "A day in the story is not a page count; leave the rest unsized.", tool: "set_length" },
|