plotcoder-board 0.1.10 → 0.1.12
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/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +72 -29
- package/src/board/readWall.d.ts +2 -0
- package/src/board/readWall.js +5 -1
- package/src/board/reducer.d.ts +3 -0
- package/src/board/reducer.js +33 -3
- package/src/board/workflows.d.ts +1 -1
- package/src/board/workflows.js +11 -11
package/package.json
CHANGED
|
@@ -52,6 +52,7 @@ import { DEFAULT_REMINDERS, titleFromBody } from "../src/board/reminders.js";
|
|
|
52
52
|
import crypto from "node:crypto";
|
|
53
53
|
import { describeRuns, describeSetups, readWall } from "../src/board/readWall.js";
|
|
54
54
|
import { GAP, ROW_WIDTH, organizePoses } from "../src/board/organize.js";
|
|
55
|
+
import { sceneLineCount } from "../src/board/paginate.js";
|
|
55
56
|
import {
|
|
56
57
|
addBoard,
|
|
57
58
|
addStructure,
|
|
@@ -434,14 +435,16 @@ async function accountReadProject() {
|
|
|
434
435
|
const { data, error } = await accountDoor.client.from("projects").select("id, record, reminders, rev").eq("id", accountDoor.projectId).maybeSingle();
|
|
435
436
|
if (error || !data || !isProjectRecord(data.record)) throw new Error(error?.message ?? "the project is gone from the account");
|
|
436
437
|
const project = normalizeProject(data.record);
|
|
437
|
-
const rows = await accountDoor.client.from("boards").select("id, state, rev").eq("project_id", project.id);
|
|
438
|
+
const rows = await accountDoor.client.from("boards").select("id, state, rev, updated_at").eq("project_id", project.id);
|
|
438
439
|
const boards = {};
|
|
439
440
|
const revs = {};
|
|
441
|
+
const changedAt = {};
|
|
440
442
|
for (const row of rows.data ?? []) {
|
|
441
443
|
if (isBoardState(row.state)) boards[row.id] = normalizeState(row.state);
|
|
442
444
|
revs[row.id] = row.rev;
|
|
445
|
+
if (row.updated_at) changedAt[row.id] = row.updated_at;
|
|
443
446
|
}
|
|
444
|
-
return { project, boards, revs, reminders: Array.isArray(data.reminders) ? data.reminders : null, rev: data.rev, base: ACCOUNT, live: ACCOUNT };
|
|
447
|
+
return { project, boards, revs, changedAt, reminders: Array.isArray(data.reminders) ? data.reminders : null, rev: data.rev, base: ACCOUNT, live: ACCOUNT };
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
async function accountWriteProject(project, boards, rev, reminders) {
|
|
@@ -786,7 +789,7 @@ const CHECK_WORDS = {
|
|
|
786
789
|
unwritten: "no card without a headline or change line",
|
|
787
790
|
unlinked: "no card without an arrow",
|
|
788
791
|
duplicate: "no two headlines alike",
|
|
789
|
-
sequence: "no group too long for one sequence",
|
|
792
|
+
sequence: "no group too long for one sequence (act groups are not asked)",
|
|
790
793
|
uncast: "nobody in the cast on no card",
|
|
791
794
|
absent: "nobody gone for a third of the story",
|
|
792
795
|
backwards: "no payoff before its setup",
|
|
@@ -803,7 +806,7 @@ function summarize(state) {
|
|
|
803
806
|
.map((note) => {
|
|
804
807
|
const cast = note.characterIds.map((id) => nameOf.get(id) ?? id);
|
|
805
808
|
const who = cast.length ? `, cast: ${cast.join(", ")}` : "";
|
|
806
|
-
const plant = note.plants ? ", plants" : "";
|
|
809
|
+
const plant = note.plants ? (note.payoffBoardId ? ", plants → pays off later" : ", plants") : "";
|
|
807
810
|
const place = note.location ? `, at: ${note.location}` : "";
|
|
808
811
|
const count = formatPages(noteEighths(note));
|
|
809
812
|
const pages = `${count} ${count === "1" ? "page" : "pages"}${isMeasured(note) ? ", written" : note.lengthEighths === null ? ", unsized" : ""}`;
|
|
@@ -841,7 +844,9 @@ function summarize(state) {
|
|
|
841
844
|
` - ${group.id} — "${group.title}" holds ${group.noteIds.length}: ${group.noteIds.join(", ")}`,
|
|
842
845
|
)
|
|
843
846
|
.join("\n");
|
|
844
|
-
const
|
|
847
|
+
const storyIndex = new Map(readingOrder(state.notes).map((note, index) => [note.id, index]));
|
|
848
|
+
const arrows = [...state.arrows]
|
|
849
|
+
.sort((a, b) => (storyIndex.get(a.from) ?? Infinity) - (storyIndex.get(b.from) ?? Infinity) || (a.kind === "setup") - (b.kind === "setup"))
|
|
845
850
|
.map(
|
|
846
851
|
(arrow) =>
|
|
847
852
|
` - ${arrow.id} [${arrow.kind ?? "follows"}] — ${arrow.from} → ${arrow.to} ("${headline(arrow.from)}" ${arrow.kind === "setup" ? "sets up" : "→"} "${headline(arrow.to)}")`,
|
|
@@ -931,7 +936,7 @@ server.registerTool(
|
|
|
931
936
|
: "board";
|
|
932
937
|
return ok(
|
|
933
938
|
`PlotCoder ${which} (${door(live, base)})\n${summarize(state)}`,
|
|
934
|
-
state,
|
|
939
|
+
{ ...state, notes: state.notes.map((note) => ({ ...note, eighths: noteEighths(note), measured: isMeasured(note) })) },
|
|
935
940
|
);
|
|
936
941
|
},
|
|
937
942
|
);
|
|
@@ -976,7 +981,7 @@ server.registerTool(
|
|
|
976
981
|
});
|
|
977
982
|
const { beats, scenes } = countRanks(state);
|
|
978
983
|
return ok(
|
|
979
|
-
`${result?.length ?? 0} card(s) are now ${args.rank}${where(live)}. The board holds ${beats} beats and ${scenes} scenes.`,
|
|
984
|
+
`${result?.length ?? 0} card(s) are now ${args.rank}${where(live)}. The board holds ${beats} beats and ${scenes} scenes. The rows are as they were; organize lays a row per beat.`,
|
|
980
985
|
result,
|
|
981
986
|
);
|
|
982
987
|
},
|
|
@@ -1000,7 +1005,7 @@ server.registerTool(
|
|
|
1000
1005
|
lengthEighths: toEighths(args.pages),
|
|
1001
1006
|
});
|
|
1002
1007
|
return ok(
|
|
1003
|
-
`${result?.length ?? 0} card(s) now run
|
|
1008
|
+
`${result?.length ?? 0} card(s) now run ${args.pages} page(s), the writer's estimate${where(live)}. The board runs about ${formatPages(boardEighths(state))} pages against a ${formatPages(state.targetEighths)}-page target.`,
|
|
1004
1009
|
result,
|
|
1005
1010
|
);
|
|
1006
1011
|
},
|
|
@@ -1185,6 +1190,7 @@ server.registerTool(
|
|
|
1185
1190
|
},
|
|
1186
1191
|
async () => {
|
|
1187
1192
|
const { state, live, base } = await readBoard();
|
|
1193
|
+
const { project: projectForRead } = await readProject();
|
|
1188
1194
|
const reading = readWall(state);
|
|
1189
1195
|
const runs = describeRuns(reading, state).map((line, index) => {
|
|
1190
1196
|
const ids = reading.runs[index]?.ids ?? [];
|
|
@@ -1222,6 +1228,7 @@ server.registerTool(
|
|
|
1222
1228
|
...(reading.setups.length
|
|
1223
1229
|
? describeSetups(reading, state).map((line) => ` - ${line}`)
|
|
1224
1230
|
: [" (no arrow is marked as a setup)"]),
|
|
1231
|
+
...reading.later.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" is folded and pays off later, on "${boardById(projectForRead, item.boardId)?.name ?? item.boardId}"`),
|
|
1225
1232
|
"questions the wall raises:",
|
|
1226
1233
|
...(reading.findings.length
|
|
1227
1234
|
? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}${finding.ids.length ? ` (ids: ${finding.ids.join(", ")})` : ""}`)
|
|
@@ -1301,8 +1308,10 @@ server.registerTool(
|
|
|
1301
1308
|
trail.push({ before: state, after: canon(final), what: `move_scene "${card.headline}"` });
|
|
1302
1309
|
undone.length = 0;
|
|
1303
1310
|
const order = readingOrder(final.notes);
|
|
1311
|
+
const group = final.groups.find((item) => item.noteIds.includes(card.id));
|
|
1312
|
+
const groupLine = group ? ` It is still in "${group.title || "an untitled group"}"; a frame does not follow a move, so say if the act or sequence should change.` : "";
|
|
1304
1313
|
return ok(
|
|
1305
|
-
`Moved "${card.headline}" to ${args.after ? "after" : "before"} "${target.headline}": ${removed} arrow(s) removed, ${drawn} drawn, the wall tidied along them${where(live)}. Story order now: ${order.map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}
|
|
1314
|
+
`Moved "${card.headline}" to ${args.after ? "after" : "before"} "${target.headline}": ${removed} arrow(s) removed, ${drawn} drawn, the wall tidied along them${where(live)}. Story order now: ${order.map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}.${groupLine} One undo takes the whole move back.`,
|
|
1306
1315
|
{ order: order.map((note) => note.id) },
|
|
1307
1316
|
);
|
|
1308
1317
|
},
|
|
@@ -1467,7 +1476,7 @@ server.registerTool(
|
|
|
1467
1476
|
{
|
|
1468
1477
|
title: "Write a scene",
|
|
1469
1478
|
description:
|
|
1470
|
-
"Write a card's scene text in Fountain — action, character cues in capitals, dialogue under them — onto the card by id. The card is then measured (its lines against a page) instead of estimated. An empty string clears it. Read read_pages first so the scene fits what is around it, and do not write scenes the writer has not asked for.",
|
|
1479
|
+
"Write a card's scene text in Fountain — action, character cues in capitals, dialogue under them — onto the card by id; the scene heading comes from the card's place, so start with the action. The card is then measured (its lines as they print against a 55-line page) instead of estimated. An empty string clears it. Read read_pages first so the scene fits what is around it, and do not write scenes the writer has not asked for.",
|
|
1471
1480
|
inputSchema: { id: z.string(), text: z.string() },
|
|
1472
1481
|
},
|
|
1473
1482
|
async (args) => {
|
|
@@ -1476,10 +1485,10 @@ server.registerTool(
|
|
|
1476
1485
|
if (!result) return ok(`No card with id ${args.id}. Call list_board.`);
|
|
1477
1486
|
return ok(`Nothing changed: "${result.headline}" already reads that way.`);
|
|
1478
1487
|
}
|
|
1479
|
-
const
|
|
1488
|
+
const printed = sceneLineCount(args.text);
|
|
1480
1489
|
return ok(
|
|
1481
|
-
`Wrote "${result.headline}": ${
|
|
1482
|
-
result,
|
|
1490
|
+
`Wrote "${result.headline}": ${printed} line(s) as they print (headings, blank lines and wrapped dialogue counted), measured at ${formatPages(noteEighths(result))} of a 55-line page, rounded to the nearest eighth and never below one${where(live)}. The heading comes from the card's place, so the text starts with the action. While the text stands the card is measured, not estimated; its estimate is untouched underneath.`,
|
|
1491
|
+
{ ...result, eighths: noteEighths(result), measured: true, printedLines: printed },
|
|
1483
1492
|
);
|
|
1484
1493
|
},
|
|
1485
1494
|
);
|
|
@@ -1505,7 +1514,8 @@ server.registerTool(
|
|
|
1505
1514
|
if (/^\.(?!\.)/.test(line) && index < ids.length) {
|
|
1506
1515
|
const note = state.notes.find((item) => item.id === ids[index]);
|
|
1507
1516
|
index += 1;
|
|
1508
|
-
|
|
1517
|
+
const standIn = note && !(note.location ?? "").trim() ? " · no place: the headline stands in for the heading" : "";
|
|
1518
|
+
lines.push(`${line} [[id: ${note?.id ?? "?"} · ${note && isMeasured(note) ? "measured" : "estimated"} ${formatPages(note ? noteEighths(note) : 0)}pp${standIn}]]`);
|
|
1509
1519
|
} else {
|
|
1510
1520
|
lines.push(line);
|
|
1511
1521
|
}
|
|
@@ -1564,7 +1574,7 @@ server.registerTool(
|
|
|
1564
1574
|
WORKFLOWS.map(
|
|
1565
1575
|
(workflow) =>
|
|
1566
1576
|
`- ${workflow.id} — ${workflow.name}\n ask: "${workflow.ask}"\n tools: ${workflow.tools.join(", ")}\n keep: ${workflow.then}${
|
|
1567
|
-
workflow.needs ? `\n the treatment should
|
|
1577
|
+
workflow.needs ? `\n the treatment should answer (ask the writer for what it leaves open; invent none of it):\n${workflow.needs.map((need) => ` - ${need.question} ${need.hint} → ${need.tool}`).join("\n")}` : ""
|
|
1568
1578
|
}`,
|
|
1569
1579
|
).join("\n"),
|
|
1570
1580
|
WORKFLOWS,
|
|
@@ -1889,10 +1899,9 @@ server.registerTool(
|
|
|
1889
1899
|
undone.push(last);
|
|
1890
1900
|
const { boardId } = await readBoard();
|
|
1891
1901
|
const live = await writeBoard(last.before, rev, base, boardId);
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
);
|
|
1902
|
+
const orderLine = /^(move_scene|organize)/.test(last.what) ? ` Story order now: ${readingOrder(last.before.notes).map((note, index) => `${index + 1}. ${note.headline}`).join(", ")}.` : "";
|
|
1903
|
+
const more = trail.length >= TRAIL_CAP ? `${trail.length} more of mine can be undone — the most I keep, so the oldest have gone` : `${trail.length} more of mine can be undone`;
|
|
1904
|
+
return ok(`Undid ${last.what}${where(live)}.${orderLine} ${more}.`, last.before);
|
|
1896
1905
|
},
|
|
1897
1906
|
);
|
|
1898
1907
|
|
|
@@ -1924,23 +1933,52 @@ server.registerTool(
|
|
|
1924
1933
|
{
|
|
1925
1934
|
title: "Fold the corner",
|
|
1926
1935
|
description:
|
|
1927
|
-
`Fold the corner of cards — mark them as planting something — or unfold them. ${wordSentence("corner")} The setup arrow is create_arrow with kind 'setup'. Folding never moves a card.`,
|
|
1936
|
+
`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, and the wall stops asking where it comes back; later '' forgets it. Folding never moves a card.`,
|
|
1928
1937
|
inputSchema: {
|
|
1929
1938
|
ids: z.array(z.string()).min(1),
|
|
1930
1939
|
plants: z.boolean(),
|
|
1940
|
+
later: z.string().optional(),
|
|
1931
1941
|
},
|
|
1932
1942
|
},
|
|
1933
1943
|
async (args) => {
|
|
1934
|
-
|
|
1944
|
+
let { result, live, changed } = await commit({
|
|
1935
1945
|
type: "set_plant",
|
|
1936
1946
|
ids: args.ids,
|
|
1937
1947
|
plants: args.plants,
|
|
1938
1948
|
});
|
|
1949
|
+
let laterLine = "";
|
|
1950
|
+
if (args.plants && args.later !== undefined) {
|
|
1951
|
+
// A series plant (R50): the fold pays off on another board of the project.
|
|
1952
|
+
// The kernel cannot check the board exists; this door can.
|
|
1953
|
+
const { project } = await readProject();
|
|
1954
|
+
const { state: now, boardId: current } = await readBoard();
|
|
1955
|
+
const here = now.notes.filter((note) => args.ids.includes(note.id));
|
|
1956
|
+
if (args.later.trim() === "") {
|
|
1957
|
+
const cleared = await commit({ type: "set_payoff_board", ids: args.ids, boardId: null });
|
|
1958
|
+
if (cleared.changed) {
|
|
1959
|
+
result = cleared.result;
|
|
1960
|
+
live = cleared.live;
|
|
1961
|
+
changed = true;
|
|
1962
|
+
laterLine = " The board it paid off on is forgotten; read_wall asks again until a setup arrow or a board pays it off.";
|
|
1963
|
+
}
|
|
1964
|
+
} else {
|
|
1965
|
+
const target = findBoard(project, args.later);
|
|
1966
|
+
if (!target) return ok(`No board matches "${args.later}". Call list_boards for the project's boards; a fold pays off later on one of them.`);
|
|
1967
|
+
if (target.id === (current ?? project.activeBoardId)) return ok(`"${target.name}" is this board. A payoff on the same board is a setup arrow: create_arrow from the fold to the scene, kind 'setup'.`);
|
|
1968
|
+
const named = await commit({ type: "set_payoff_board", ids: args.ids, boardId: target.id });
|
|
1969
|
+
if (named.changed) {
|
|
1970
|
+
result = named.result;
|
|
1971
|
+
live = named.live;
|
|
1972
|
+
changed = true;
|
|
1973
|
+
}
|
|
1974
|
+
laterLine = ` ${here.length} card(s) pay off later, on "${target.name}": read_wall stops asking where they come back, and the card says so.`;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1939
1977
|
const count = result?.length ?? 0;
|
|
1940
|
-
if (count === 0) return ok("No change: those cards were already that way, or the ids are not on the board.");
|
|
1978
|
+
if (!changed || count === 0) return ok("No change: those cards were already that way, or the ids are not on the board.");
|
|
1941
1979
|
return ok(
|
|
1942
1980
|
args.plants
|
|
1943
|
-
? `${count} card(s) now plant something${where(live)}
|
|
1981
|
+
? `${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."}`
|
|
1944
1982
|
: `${count} card(s) no longer marked as planting${where(live)}.`,
|
|
1945
1983
|
result,
|
|
1946
1984
|
);
|
|
@@ -1977,6 +2015,7 @@ server.registerTool(
|
|
|
1977
2015
|
inputSchema: { id: z.string(), name: z.string().min(1) },
|
|
1978
2016
|
},
|
|
1979
2017
|
async (args) => {
|
|
2018
|
+
const { state: before } = await readBoard();
|
|
1980
2019
|
const { state, changed, result, live } = await commit({
|
|
1981
2020
|
type: "rename_character",
|
|
1982
2021
|
id: args.id,
|
|
@@ -1989,7 +2028,10 @@ server.registerTool(
|
|
|
1989
2028
|
: ok(`No character with id ${args.id}. Call list_board for the cast.`);
|
|
1990
2029
|
}
|
|
1991
2030
|
const followed = state.notes.filter((note) => note.characterIds.includes(result.id)).length;
|
|
1992
|
-
|
|
2031
|
+
const oldName = (before.characters.find((item) => item.id === result.id)?.name ?? "").trim();
|
|
2032
|
+
const stale = oldName ? CHARACTER_FIELDS.filter((field) => (result[field] ?? "").toLowerCase().includes(oldName.toLowerCase())) : [];
|
|
2033
|
+
const staleLine = stale.length ? ` The page's ${stale.join(", ")} still mention${stale.length === 1 ? "s" : ""} "${oldName}"; the page is untouched.` : "";
|
|
2034
|
+
return ok(`Renamed to "${result.name}" (${result.id})${where(live)}; the name changed on ${followed} card${followed === 1 ? "" : "s"}.${staleLine}`, result);
|
|
1993
2035
|
},
|
|
1994
2036
|
);
|
|
1995
2037
|
|
|
@@ -2297,7 +2339,7 @@ server.registerTool(
|
|
|
2297
2339
|
|
|
2298
2340
|
// --- The project ------------------------------------------------------
|
|
2299
2341
|
|
|
2300
|
-
function describeBoards(project, boards) {
|
|
2342
|
+
function describeBoards(project, boards, changedAt = null) {
|
|
2301
2343
|
return project.boards
|
|
2302
2344
|
.map((board, index) => {
|
|
2303
2345
|
const state = boards[board.id];
|
|
@@ -2306,7 +2348,8 @@ function describeBoards(project, boards) {
|
|
|
2306
2348
|
state && isBoardState(state)
|
|
2307
2349
|
? `${state.notes.length} cards, about ${formatPages(boardEighths(normalizeState(state)))} of ${formatPages(normalizeState(state).targetEighths)} pages`
|
|
2308
2350
|
: "no cards";
|
|
2309
|
-
|
|
2351
|
+
const changed = changedAt?.[board.id] ? `, last changed ${changedAt[board.id]}` : "";
|
|
2352
|
+
return ` ${index + 1}. ${board.id} — "${board.name}"${open}: ${shape}${changed}`;
|
|
2310
2353
|
})
|
|
2311
2354
|
.join("\n");
|
|
2312
2355
|
}
|
|
@@ -2320,13 +2363,13 @@ server.registerTool(
|
|
|
2320
2363
|
inputSchema: {},
|
|
2321
2364
|
},
|
|
2322
2365
|
async () => {
|
|
2323
|
-
const { project, boards, live, base } = await readProject();
|
|
2366
|
+
const { project, boards, live, base, changedAt } = await readProject();
|
|
2324
2367
|
return ok(
|
|
2325
2368
|
[
|
|
2326
2369
|
`Project "${project.name}" (${door(live, base)})`,
|
|
2327
2370
|
`premise: ${project.premise ? `"${project.premise}"` : "(not set)"}`,
|
|
2328
2371
|
`boards: ${project.boards.length}`,
|
|
2329
|
-
describeBoards(project, boards),
|
|
2372
|
+
describeBoards(project, boards, changedAt),
|
|
2330
2373
|
].join("\n"),
|
|
2331
2374
|
project,
|
|
2332
2375
|
);
|
|
@@ -2665,7 +2708,7 @@ server.registerTool(
|
|
|
2665
2708
|
const { project, boards, reminders } = await readProject();
|
|
2666
2709
|
const file = toProjectFile({ project, boards, reminders: reminders ?? null });
|
|
2667
2710
|
const cards = countCards(boards);
|
|
2668
|
-
const what = `"${project.name}": ${project.boards.length} board(s), ${cards} card(s)${reminders?.length ? `, ${reminders.length} reminder(s)` : ""}${project.structures?.length ? `, ${project.structures.length} structure(s)` : ""}. Pictures and takes on the account are not in the file`;
|
|
2711
|
+
const what = `"${project.name}": ${project.boards.length} board(s), ${cards} card(s)${reminders?.length ? `, ${reminders.length} reminder(s)` : ", no reminders of the writer's own (none to write)"}${project.structures?.length ? `, ${project.structures.length} structure(s)` : ", no structures of the writer's own (none to write)"}. Pictures and takes on the account are not in the file`;
|
|
2669
2712
|
if (args.path) {
|
|
2670
2713
|
fs.mkdirSync(path.dirname(path.resolve(args.path)), { recursive: true });
|
|
2671
2714
|
fs.writeFileSync(args.path, JSON.stringify(file, null, 2));
|
package/src/board/readWall.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type WallReading = {
|
|
|
61
61
|
/** Every planted card: the scene that pays it off (first setup arrow, by wall order), or null while unpaid. */
|
|
62
62
|
/** For each folded card, the cards its setup arrows land on, in wall order; empty when unpaid. */
|
|
63
63
|
payoffs: Record<string, string[]>;
|
|
64
|
+
/** Folded cards that pay off on another board of the project (R50): the card and the board. */
|
|
65
|
+
later: { id: string; boardId: string }[];
|
|
64
66
|
findings: Finding[];
|
|
65
67
|
};
|
|
66
68
|
|
package/src/board/readWall.js
CHANGED
|
@@ -307,8 +307,11 @@ export function readWall(state) {
|
|
|
307
307
|
// pays off at the cash and again at the initials), and both count.
|
|
308
308
|
payoffs[note.id] = heads;
|
|
309
309
|
}
|
|
310
|
+
// A fold that pays off on another board (R50) is not unpaid: it is listed
|
|
311
|
+
// under `later`, and the door that knows the project names the board.
|
|
312
|
+
const later = order.filter((note) => note.plants && note.payoffBoardId).map((note) => ({ id: note.id, boardId: note.payoffBoardId }));
|
|
310
313
|
for (const note of order) {
|
|
311
|
-
if (note.plants && !paysOff.has(note.id)) {
|
|
314
|
+
if (note.plants && !paysOff.has(note.id) && !note.payoffBoardId) {
|
|
312
315
|
findings.push({
|
|
313
316
|
kind: "unpaid",
|
|
314
317
|
ids: [note.id],
|
|
@@ -372,6 +375,7 @@ export function readWall(state) {
|
|
|
372
375
|
runs,
|
|
373
376
|
setups,
|
|
374
377
|
payoffs,
|
|
378
|
+
later,
|
|
375
379
|
findings,
|
|
376
380
|
};
|
|
377
381
|
}
|
package/src/board/reducer.d.ts
CHANGED
|
@@ -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
|
+
/** When folded: the id of another board of the project where it pays off (R50), or null. */
|
|
83
|
+
payoffBoardId: string | null;
|
|
82
84
|
/** Where the scene happens (R37): a phrase in the writer's words; empty until set. */
|
|
83
85
|
location: string;
|
|
84
86
|
/** The scene's text in Fountain (R23 b): action, cues, dialogue; empty until written. */
|
|
@@ -161,6 +163,7 @@ export type Command =
|
|
|
161
163
|
| ({ type: "update_character"; id: string } & Partial<Record<CharacterField, string>>)
|
|
162
164
|
| { type: "set_cast"; ids: string[]; characterIds: string[] }
|
|
163
165
|
| { type: "set_plant"; ids: string[]; plants: boolean }
|
|
166
|
+
| { type: "set_payoff_board"; ids: string[]; boardId: string | null }
|
|
164
167
|
| { type: "set_location"; ids: string[]; location: string }
|
|
165
168
|
| { type: "apply_template"; template: string; beats?: Array<{ name: string; prompt: string; at: number }> }
|
|
166
169
|
| { type: "set_text"; id: string; text: string }
|
package/src/board/reducer.js
CHANGED
|
@@ -233,6 +233,9 @@ export function seedState(now = nowIso()) {
|
|
|
233
233
|
location: "",
|
|
234
234
|
text: "",
|
|
235
235
|
plants: false,
|
|
236
|
+
// A fold that pays off on another board — a later episode — names it here;
|
|
237
|
+
// null claims nothing (R50).
|
|
238
|
+
payoffBoardId: null,
|
|
236
239
|
createdAt: now,
|
|
237
240
|
updatedAt: now,
|
|
238
241
|
});
|
|
@@ -325,6 +328,8 @@ export function normalizeState(value) {
|
|
|
325
328
|
const characterIds = knownCast(note?.characterIds, characters);
|
|
326
329
|
// Cards written before R31 have no fold; a plant is a claim you make.
|
|
327
330
|
const plants = note?.plants === true;
|
|
331
|
+
// Cards written before R50 pay off on their own board or not at all.
|
|
332
|
+
const payoffBoardId = plants && typeof note?.payoffBoardId === "string" && note.payoffBoardId ? note.payoffBoardId : null;
|
|
328
333
|
// Cards written before R37 have no place; a scene is nowhere until it is.
|
|
329
334
|
const location = typeof note?.location === "string" ? note.location : "";
|
|
330
335
|
// Cards written before pages (R23 b) have no text; a scene is unwritten until it is.
|
|
@@ -336,13 +341,14 @@ export function normalizeState(value) {
|
|
|
336
341
|
Array.isArray(note.characterIds) &&
|
|
337
342
|
sameIds(note.characterIds, characterIds) &&
|
|
338
343
|
note.plants === plants &&
|
|
344
|
+
note.payoffBoardId === payoffBoardId &&
|
|
339
345
|
note.location === location &&
|
|
340
346
|
note.text === text
|
|
341
347
|
) {
|
|
342
348
|
return note;
|
|
343
349
|
}
|
|
344
350
|
patched = true;
|
|
345
|
-
return { ...note, rank, lengthEighths, characterIds, plants, location, text };
|
|
351
|
+
return { ...note, rank, lengthEighths, characterIds, plants, payoffBoardId, location, text };
|
|
346
352
|
});
|
|
347
353
|
|
|
348
354
|
// Boards written before the production half (Roadmap 2, item 8) have no
|
|
@@ -416,6 +422,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
416
422
|
: clampEighths(command.lengthEighths, DEFAULT_NOTE_EIGHTHS, MAX_NOTE_EIGHTHS),
|
|
417
423
|
characterIds: knownCast(command.characterIds, state.characters ?? []),
|
|
418
424
|
plants: command.plants === true,
|
|
425
|
+
payoffBoardId: null,
|
|
419
426
|
location: cleanPlace(command.location),
|
|
420
427
|
text: typeof command.text === "string" ? command.text : "",
|
|
421
428
|
z: maxZ(state.notes) + 1,
|
|
@@ -811,6 +818,7 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
811
818
|
lengthEighths: null,
|
|
812
819
|
characterIds: [],
|
|
813
820
|
plants: false,
|
|
821
|
+
payoffBoardId: null,
|
|
814
822
|
location: "",
|
|
815
823
|
text: "",
|
|
816
824
|
createdAt: now,
|
|
@@ -895,8 +903,30 @@ export function applyCommand(state, command, now = nowIso()) {
|
|
|
895
903
|
const plants = command.plants === true;
|
|
896
904
|
const touched = [];
|
|
897
905
|
const notes = state.notes.map((note) => {
|
|
898
|
-
if (!ids.has(note.id)
|
|
899
|
-
|
|
906
|
+
if (!ids.has(note.id)) return note;
|
|
907
|
+
// Unfolding forgets where it paid off; a claim that no longer stands.
|
|
908
|
+
const payoffBoardId = plants ? note.payoffBoardId : null;
|
|
909
|
+
if (note.plants === plants && note.payoffBoardId === payoffBoardId) return note;
|
|
910
|
+
const next = bump(note, { plants, payoffBoardId }, now);
|
|
911
|
+
touched.push(next);
|
|
912
|
+
return next;
|
|
913
|
+
});
|
|
914
|
+
if (touched.length === 0) return { state, changed: false };
|
|
915
|
+
return { state: { ...state, notes }, changed: true, result: touched };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// A fold that pays off on another board of the project (R50): the wall
|
|
919
|
+
// stops asking where it comes back, and the reading says where. The
|
|
920
|
+
// kernel cannot check the board exists; the door that knows the project
|
|
921
|
+
// does. Null takes the claim back.
|
|
922
|
+
case "set_payoff_board": {
|
|
923
|
+
const ids = new Set(command.ids);
|
|
924
|
+
if (ids.size === 0) return { state, changed: false };
|
|
925
|
+
const payoffBoardId = typeof command.boardId === "string" && command.boardId ? command.boardId : null;
|
|
926
|
+
const touched = [];
|
|
927
|
+
const notes = state.notes.map((note) => {
|
|
928
|
+
if (!ids.has(note.id) || !note.plants || note.payoffBoardId === payoffBoardId) return note;
|
|
929
|
+
const next = bump(note, { payoffBoardId }, now);
|
|
900
930
|
touched.push(next);
|
|
901
931
|
return next;
|
|
902
932
|
});
|
package/src/board/workflows.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Workflow = {
|
|
|
12
12
|
/** The rule the agent keeps while doing it. */
|
|
13
13
|
then: string;
|
|
14
14
|
/** What the writer's material should say for the workflow to need no questions back, and the tool each answer lands in (R49). */
|
|
15
|
-
needs?: {
|
|
15
|
+
needs?: { question: string; hint: string; tool: string }[];
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export declare const WORKFLOWS: readonly Workflow[];
|
package/src/board/workflows.js
CHANGED
|
@@ -23,17 +23,17 @@ export const WORKFLOWS = [
|
|
|
23
23
|
// who answers them in the treatment gets a wall with no questions back;
|
|
24
24
|
// an agent asks the ones the treatment leaves open, and invents none.
|
|
25
25
|
needs: [
|
|
26
|
-
{
|
|
27
|
-
{
|
|
28
|
-
{
|
|
29
|
-
{
|
|
30
|
-
{
|
|
31
|
-
{
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
{
|
|
35
|
-
{
|
|
36
|
-
{
|
|
26
|
+
{ question: "How long is it?", hint: "An hour, a half-hour, a feature — or a page count, if you have one.", tool: "set_target" },
|
|
27
|
+
{ question: "What is the central question, in one sentence?", hint: "And if this is one episode of something, what is the series about?", tool: "set_logline, set_premise" },
|
|
28
|
+
{ question: "Which scenes are the turns?", hint: "Name them, or say \"propose them and I will strike\".", tool: "set_rank" },
|
|
29
|
+
{ question: "Does it have acts?", hint: "If so, where does each break fall?", tool: "create_group" },
|
|
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 for now.", tool: "the headline" },
|
|
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
|
+
{ 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
|
+
{ 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" },
|
|
35
|
+
{ question: "What are the project and the board called?", hint: "The series, and this episode.", tool: "rename_project, rename_board" },
|
|
36
|
+
{ question: "What must not be invented?", hint: "Looks and voices are yours until you say; so is anything the treatment does not state.", tool: "update_character, later" },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
39
|
{
|