plotcoder-board 0.1.29 → 0.1.30
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 +8 -4
- package/package.json +1 -1
- package/scripts/plotcoder-mcp-server.mjs +40 -24
- package/src/board/agents.js +2 -2
- package/src/board/readWall.js +12 -3
- package/src/board/workflows.js +1 -1
package/README.md
CHANGED
|
@@ -51,11 +51,15 @@ An agent should call the tools, never fake mouse drags. The skill in `.cursor/sk
|
|
|
51
51
|
A **blind run** is a fresh agent given the on-ramp and a treatment and nothing
|
|
52
52
|
else, asked to build a wall and to keep a log of everything that made the job
|
|
53
53
|
harder than it should have been. The friction log is the product; the wall is
|
|
54
|
-
just what produces it.
|
|
54
|
+
just what produces it. Nineteen rounds have been run, the first three through a
|
|
55
55
|
repo checkout and the rest through the account door; every finding from the
|
|
56
|
-
first seventeen is fixed or decided
|
|
56
|
+
first seventeen is fixed or decided. Round seventeen, the first to start from
|
|
57
57
|
a page of notes instead of a treatment, found the wall has no way to hold a
|
|
58
|
-
maybe — the open card, R59,
|
|
58
|
+
maybe — the open card, R59, built from it; round eighteen confirmed that a
|
|
59
|
+
thread is not a fold — the thread, R60, built from it, a named string through
|
|
60
|
+
cards with either end open until the writer ties it; round nineteen measured
|
|
61
|
+
the thread and found the fold beside it cannot say which of two things a
|
|
62
|
+
scene plants. [`blind-runs/`](blind-runs/) holds the rules that keep a round honest,
|
|
59
63
|
the table of rounds, and the next round's prompt with the test account filled in.
|
|
60
64
|
|
|
61
65
|
A round works a **test account** — a throwaway marked on its writer row, and the
|
|
@@ -100,4 +104,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
|
|
|
100
104
|
|
|
101
105
|
## Status
|
|
102
106
|
|
|
103
|
-
Version 0.1.
|
|
107
|
+
Version 0.1.30. 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
|
@@ -991,6 +991,19 @@ const CHECK_WORDS = {
|
|
|
991
991
|
unplaced: "no card without a place",
|
|
992
992
|
loose: "no thread with a loose end",
|
|
993
993
|
};
|
|
994
|
+
/** What an open card would be asked once closed, as the question and not the check's clean form (round nineteen, entry 31). */
|
|
995
|
+
const ASK_WORDS = {
|
|
996
|
+
unwritten: "its change line",
|
|
997
|
+
unlinked: "what comes before and after it",
|
|
998
|
+
duplicate: "which of two alike headlines it is",
|
|
999
|
+
sequence: "whether its group is one sequence",
|
|
1000
|
+
nobody: "who is in it",
|
|
1001
|
+
absent: "who has been gone too long",
|
|
1002
|
+
backwards: "why its payoff comes before its setup",
|
|
1003
|
+
unpaid: "where its fold pays off",
|
|
1004
|
+
unplanted: "what plants its payoff",
|
|
1005
|
+
unplaced: "where it happens",
|
|
1006
|
+
};
|
|
994
1007
|
const SAMPLE_NOTE = "sample: this is the wall PlotCoder starts with (Maya, Tom, the letter); nothing here is the writer's. Replace it, or new_board.";
|
|
995
1008
|
|
|
996
1009
|
// --- Reporting -------------------------------------------------------------
|
|
@@ -1051,7 +1064,7 @@ function summarize(state) {
|
|
|
1051
1064
|
const revised = snap && (snap.headline !== note.headline || snap.change !== note.change || (snap.text ?? "") !== (note.text ?? "") || (snap.location ?? "") !== (note.location ?? "")) ? `, changed in ${state.revision.color}` : "";
|
|
1052
1065
|
const place = note.location ? `, at: ${note.location}` : "";
|
|
1053
1066
|
const when = note.when ? `, when: ${note.when}` : "";
|
|
1054
|
-
const openWord = note.open ? `, open: ${note.open}` : "";
|
|
1067
|
+
const openWord = note.open ? `, open (the writer's words): "${note.open}"` : "";
|
|
1055
1068
|
const count = formatPages(noteEighths(note));
|
|
1056
1069
|
// A written card's estimate is kept underneath for when the text goes; say it, or it is invisible (round sixteen, entry 44).
|
|
1057
1070
|
const underneath = isMeasured(note) && note.lengthEighths !== null ? `; the writer's estimate underneath: ${formatPages(note.lengthEighths)}` : "";
|
|
@@ -1127,19 +1140,19 @@ function summarize(state) {
|
|
|
1127
1140
|
`beats: ${beats}, scenes: ${scenes}`,
|
|
1128
1141
|
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
1129
1142
|
? `runtime: about ${formatPages(runtime)} pages (an estimate from the cards; a page runs about a minute); no target set — set_target for a pilot (60) or a half-hour (30); against the feature default of 120 it would be ${formatPages(-over)} under${runtimeKinds(state)}`
|
|
1130
|
-
: `runtime: about ${formatPages(runtime)} pages of
|
|
1143
|
+
: `runtime: about ${formatPages(runtime)} pages of the ${formatPages(state.targetEighths)}-page target the writer set (set_target changes it) — ${over > 0 ? `${formatPages(over)} over` : over < 0 ? `${formatPages(-over)} under` : "on it"} (an estimate from the cards; a page runs about a minute)${runtimeKinds(state)}`,
|
|
1131
1144
|
`notes: ${state.notes.length}, groups: ${state.groups.length}, arrows: ${state.arrows.length}, cast: ${state.characters.length}`,
|
|
1132
1145
|
"cards (in story order — the follows arrows over the rows; each with its id):",
|
|
1133
1146
|
notes || " (no cards)",
|
|
1134
1147
|
"rows on the wall (top to bottom, left to right; ★ a beat):",
|
|
1135
1148
|
...(rowLines.length ? rowLines : [" (no cards)"]),
|
|
1136
|
-
"cast (the project's; every board of it casts from here):",
|
|
1149
|
+
"cast (the project's; every board of it casts from here; read_character reads a person's whole page, update_character writes it):",
|
|
1137
1150
|
cast || " (no one yet — add_character to start the roster)",
|
|
1138
1151
|
"places (each phrase is its own place, and the app relates none of them — if two are one place, set_location them the same):",
|
|
1139
1152
|
places || " (no card says where it happens yet)",
|
|
1140
1153
|
"groups:",
|
|
1141
1154
|
groups || " (no groups)",
|
|
1142
|
-
"threads (
|
|
1155
|
+
"threads (a named string through cards, in story order; an open end is asked about by the reading):",
|
|
1143
1156
|
...((state.threads ?? []).length ? state.threads.map((thread) => ` - ${threadLine(state, thread)}`) : [" (no threads — create_thread names one)"]),
|
|
1144
1157
|
"arrows:",
|
|
1145
1158
|
arrows || " (no arrows)",
|
|
@@ -1387,7 +1400,8 @@ server.registerTool(
|
|
|
1387
1400
|
}
|
|
1388
1401
|
}
|
|
1389
1402
|
// Wired into the story where the writer said, in the same frame.
|
|
1390
|
-
|
|
1403
|
+
// On a wall with no follows arrows yet, after or before draws the first one (round nineteen, entry 14).
|
|
1404
|
+
if (beside && made?.id) {
|
|
1391
1405
|
// Count the rewiring, so the reply can say it as move_scene does (round eighteen, entry 36).
|
|
1392
1406
|
const run = (command) => {
|
|
1393
1407
|
const done = step(command);
|
|
@@ -1413,9 +1427,7 @@ server.registerTool(
|
|
|
1413
1427
|
].filter(Boolean).join(", ");
|
|
1414
1428
|
// Where it landed matters only until the tidy, so the reply says the rule once and never the coordinates (round fourteen, entry 11).
|
|
1415
1429
|
const placed = beside
|
|
1416
|
-
? wallHasFollows
|
|
1417
|
-
? ` Wired ${args.after ? "after" : "before"} "${beside.headline}" in the story (${removedArrows} follows arrow${removedArrows === 1 ? "" : "s"} removed, ${drawnArrows} drawn)${joinedGroup ? `, in "${joinedGroup}"` : ""}, and the wall tidied.`
|
|
1418
|
-
: ` The wall has no follows arrows, so "${args.after ? "after" : "before"}" has no story to land in: it sits after the last card; create_arrow the sequence, then move_scene.`
|
|
1430
|
+
? ` Wired ${args.after ? "after" : "before"} "${beside.headline}" in the story (${removedArrows} follows arrow${removedArrows === 1 ? "" : "s"} removed, ${drawnArrows} drawn${wallHasFollows ? "" : "; the wall's first, so the story order starts here"})${joinedGroup ? `, in "${joinedGroup}"` : ""}, and the wall tidied.`
|
|
1419
1431
|
: args.x === undefined && args.y === undefined ? ` Placed after the last card in story order.${once("placed", " organize lays the wall out along the arrows.")}` : "";
|
|
1420
1432
|
// Under a lock a new scene has a letter, not a number: say it, since the board is the only other place to learn it (round fourteen, entry 44).
|
|
1421
1433
|
const numbered = after?.lock && result?.id ? ` Numbered ${sceneNumbers(storyOrder(after), after.lock).get(result.id)} (the numbers are locked; a new scene's letter is its place between locked ones now, worked out again from where it sits if it moves; the locked numbers never move).` : "";
|
|
@@ -1452,7 +1464,8 @@ server.registerTool(
|
|
|
1452
1464
|
.map(([field, word]) => `${word}: "${prior?.[field] ?? ""}" → "${result[field] ?? ""}"`);
|
|
1453
1465
|
if (!changed || fields.length === 0) return ok(`Nothing changed on "${result.headline}": the card already read that way.`);
|
|
1454
1466
|
const mark = state.revision && state.revision.snapshot?.[result.id] && (state.revision.snapshot[result.id].headline !== result.headline || state.revision.snapshot[result.id].change !== result.change) ? ` Marked changed in the ${state.revision.color} revision "${state.revision.name}".` : "";
|
|
1455
|
-
|
|
1467
|
+
const placeNote = args.location !== undefined && (prior?.location ?? "") !== (result.location ?? "") ? nearPlaces(state, result.location) : "";
|
|
1468
|
+
return ok(`Updated "${result.headline}" — ${fields.join("; ")}${where(live)}.${mark}${placeNote}`, result);
|
|
1456
1469
|
},
|
|
1457
1470
|
);
|
|
1458
1471
|
|
|
@@ -1558,7 +1571,7 @@ server.registerTool(
|
|
|
1558
1571
|
"the cast and the places are list_board's, not the reading's",
|
|
1559
1572
|
state.targetEighths === DEFAULT_TARGET_EIGHTHS
|
|
1560
1573
|
? `runtime: about ${formatPages(boardEighths(state))} pages (${whose || "no cards"}); no target set (set_target)`
|
|
1561
|
-
: `runtime: about ${formatPages(boardEighths(state))} pages of
|
|
1574
|
+
: `runtime: about ${formatPages(boardEighths(state))} pages of the ${formatPages(state.targetEighths)}-page target the writer set (set_target changes it) — ${boardEighths(state) > state.targetEighths ? `${formatPages(boardEighths(state) - state.targetEighths)} over` : boardEighths(state) < state.targetEighths ? `${formatPages(state.targetEighths - boardEighths(state))} under` : "on it"} (${whose || "no cards"}; page_count is the script so far)`,
|
|
1562
1575
|
`groups: ${
|
|
1563
1576
|
state.groups.length
|
|
1564
1577
|
? state.groups
|
|
@@ -1587,12 +1600,12 @@ server.registerTool(
|
|
|
1587
1600
|
...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}"${item.noteId ? `, at ${episodeLabel(projectForRead, boardsNow, item.boardId, item.noteId)} "${boardsNow[item.boardId]?.notes?.find((note) => note.id === item.noteId)?.headline ?? item.noteId}"` : " — no scene there claims it yet"}`),
|
|
1588
1601
|
...reading.paidBy.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" pays off "${item.fromHeadline}" from "${item.fromBoardName}" (${episodeLabel(projectForRead, boardsNow, item.fromBoardId, item.fromNoteId)}), one board earlier`),
|
|
1589
1602
|
...(reading.open.length
|
|
1590
|
-
? ["open, by the writer's word (listed, not asked about while the words stand; set_open with \"\" closes):", ...reading.open.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" — ${item.words}${item.hides.length ? ` (would be asked
|
|
1603
|
+
? ["open, by the writer's word (listed, not asked about while the words stand; set_open with \"\" closes):", ...reading.open.map((item) => ` - "${state.notes.find((note) => note.id === item.id)?.headline ?? item.id}" — ${item.words}${item.hides.length ? ` (closed, it would be asked ${item.hides.map((kind) => ASK_WORDS[kind] ?? CHECK_WORDS[kind] ?? kind).join("; ")})` : ""}`)]
|
|
1591
1604
|
: []),
|
|
1592
1605
|
...(reading.threads.length
|
|
1593
1606
|
? ["threads (the writer's strings through the story; a loose end is asked about below):", ...reading.threads.map((thread) => ` - "${thread.name}": ${thread.ids.length ? thread.ids.map((id) => `"${state.notes.find((note) => note.id === id)?.headline ?? id}"`).join(" → ") : "no card yet"}${thread.startOpen ? " — starts nowhere yet" : ""}${thread.endOpen ? " — ends nowhere yet" : ""}`)]
|
|
1594
1607
|
: []),
|
|
1595
|
-
"questions the wall raises:",
|
|
1608
|
+
"questions the wall raises (each stands on every reading until the wall changes to answer it, or the writer leaves it — leave_question, with their reason):",
|
|
1596
1609
|
...(reading.findings.length
|
|
1597
1610
|
? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}${finding.ids.length ? ` (ids: ${finding.ids.join(", ")})` : ""}`)
|
|
1598
1611
|
: [reading.left.length ? " (none the writer has not left)" : " (none that this reading can see)"]),
|
|
@@ -2026,7 +2039,7 @@ server.registerTool(
|
|
|
2026
2039
|
const allMeasured = state.notes.length > 0 && state.notes.every((note) => isMeasured(note));
|
|
2027
2040
|
const short = state.targetEighths > 0 && boardEighths(state) * 2 < state.targetEighths;
|
|
2028
2041
|
const lines = [
|
|
2029
|
-
`"${chosen.name}" beside this wall's ${beats} beat${beats === 1 ? "" : "s"}, of ${formatPages(state.targetEighths)} pages (the story so far runs to p. ${comparison.soFar}${allMeasured ? ", measured" : ", an estimate: unsized cards read as a page each"}); a match is the nearest of the wall's beats within ${MATCH_PAGES} pages, one to one and in order:`,
|
|
2042
|
+
`"${chosen.name}" beside this wall's ${beats} beat${beats === 1 ? "" : "s"}, of ${formatPages(state.targetEighths)} pages (the story so far runs to p. ${comparison.soFar}, the page its last card ends on — read_wall counts the same cards as about ${formatPages(boardEighths(state))} pages${allMeasured ? ", measured" : ", an estimate: unsized cards read as a page each"}); a match is the nearest of the wall's beats within ${MATCH_PAGES} pages, one to one and in order:`,
|
|
2030
2043
|
// On a wall under half its target the pairing is arithmetic; say so before the rows, not after them (round eighteen, entry 52).
|
|
2031
2044
|
...(short ? [`the wall runs to less than half its target, so its beats sit early and the ${MATCH_PAGES}-page window pairs them with the structure's first beats by arithmetic; the pairing says more once the cards are sized or written, and whether a turn is missing is the writer's call, not this reading's`] : []),
|
|
2032
2045
|
...describeComparison(comparison).map((line) => ` - ${line}`),
|
|
@@ -2951,7 +2964,7 @@ server.registerTool(
|
|
|
2951
2964
|
});
|
|
2952
2965
|
const total = parts.reduce((sum, part) => sum + part.on.length, 0);
|
|
2953
2966
|
// An open card reads as open on a person's page too (round eighteen, entry 53).
|
|
2954
|
-
const where_ = (note) => [note.location ? `at ${note.location}` : "", note.when ? note.when : "", note.rank === "beat" ? "beat" : "", note.open ? `open: ${note.open}` : ""].filter(Boolean).join(" · ");
|
|
2967
|
+
const where_ = (note) => [note.location ? `at ${note.location}` : "", note.when ? note.when : "", note.rank === "beat" ? "beat" : "", note.open ? `open: "${note.open}"` : ""].filter(Boolean).join(" · ");
|
|
2955
2968
|
// A read opens with the door it came through, like every reading (round sixteen, entry 28); one scene a line (29).
|
|
2956
2969
|
const lines = [
|
|
2957
2970
|
`PlotCoder cast (${door(live, base)})`,
|
|
@@ -3009,6 +3022,17 @@ server.registerTool(
|
|
|
3009
3022
|
},
|
|
3010
3023
|
);
|
|
3011
3024
|
|
|
3025
|
+
/** A near match on the wall is probably the same place spelled twice (round ten; update_note too, round nineteen entry 36). */
|
|
3026
|
+
function nearPlaces(state, place) {
|
|
3027
|
+
const wanted = (place ?? "").trim().toLowerCase();
|
|
3028
|
+
const near = wanted
|
|
3029
|
+
? [...new Set(state.notes.map((note) => (note.location ?? "").trim()).filter(Boolean))].filter(
|
|
3030
|
+
(other) => other.toLowerCase() !== wanted && (other.toLowerCase().includes(wanted) || wanted.includes(other.toLowerCase())),
|
|
3031
|
+
)
|
|
3032
|
+
: [];
|
|
3033
|
+
return near.length ? ` The wall also has ${near.map((other) => `"${other}"`).join(", ")} — the same place spelled twice, or two places? Each distinct phrase counts as one place.` : "";
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3012
3036
|
server.registerTool(
|
|
3013
3037
|
"set_location",
|
|
3014
3038
|
{
|
|
@@ -3029,16 +3053,8 @@ server.registerTool(
|
|
|
3029
3053
|
return ok("No place changed: those cards already read that way.");
|
|
3030
3054
|
}
|
|
3031
3055
|
const place = result[0].location;
|
|
3032
|
-
// A near match on the wall is probably the same place spelled twice.
|
|
3033
|
-
const wanted = (place ?? "").trim().toLowerCase();
|
|
3034
|
-
const near = wanted
|
|
3035
|
-
? [...new Set(state.notes.map((note) => (note.location ?? "").trim()).filter(Boolean))].filter(
|
|
3036
|
-
(other) => other.toLowerCase() !== wanted && (other.toLowerCase().includes(wanted) || wanted.includes(other.toLowerCase())),
|
|
3037
|
-
)
|
|
3038
|
-
: [];
|
|
3039
|
-
const warn = near.length ? ` The wall also has ${near.map((other) => `"${other}"`).join(", ")} — the same place spelled twice, or two places? Each distinct phrase counts as one place.` : "";
|
|
3040
3056
|
return ok(
|
|
3041
|
-
`${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${
|
|
3057
|
+
`${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${nearPlaces(state, place)}${stillOpen(result)}`,
|
|
3042
3058
|
result,
|
|
3043
3059
|
);
|
|
3044
3060
|
},
|
|
@@ -3780,7 +3796,7 @@ server.registerTool(
|
|
|
3780
3796
|
{
|
|
3781
3797
|
title: "Save the project as a file",
|
|
3782
3798
|
description:
|
|
3783
|
-
|
|
3799
|
+
`The project the server is working, as the file Save project writes and Open project takes: the record, every board with its cards, the reminders and the writer's structures. Pass path to write it (a .json) — an absolute path, since a relative one resolves from the folder the server was started in, which is ${process.cwd()}, not yours; without a path, the reply's JSON is the file. Pictures and takes on the account are not in the file. Works through every door.`,
|
|
3784
3800
|
inputSchema: { path: z.string().optional() },
|
|
3785
3801
|
},
|
|
3786
3802
|
async (args) => {
|
package/src/board/agents.js
CHANGED
|
@@ -55,7 +55,7 @@ export const AGENTS = {
|
|
|
55
55
|
"No opinions about how many beats there should be. Marking the turns a treatment plainly makes is reading it, not an opinion: mark them, say which, and let the writer strike or add.",
|
|
56
56
|
"Page counts are estimates.",
|
|
57
57
|
"Ask before delete_board, delete_project, empty_account, delete_account, unlock_numbers, remove_file, an import_project that replaces, or claim_account — the writer gives the email and the password; never invent one. export_project first, when something might be wanted back.",
|
|
58
|
-
"Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict.",
|
|
58
|
+
"Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict. A thing the writer has not decided is an open card in their words (set_open), never a guess to fill the field; a thing whose far end the writer knows and not where it is first seen — the letter, the ring — is a thread (create_thread) with an open start, and the wall asks from that end.",
|
|
59
59
|
],
|
|
60
60
|
person:
|
|
61
61
|
"Give your agent the account door only on a machine you trust; it signs in as you and shows under People as “an agent, as you” while it runs. Wire the server before you start the agent's session, with the two sign-in lines beside it, and the agent has every tool from its first message; wired from inside a session, the server connects only on the next one. Your agent can also make your account: give it your email and a password of your choosing.",
|
|
@@ -68,7 +68,7 @@ export function agentsAsText() {
|
|
|
68
68
|
// The calls and the rules first, the doors after: an agent with the tools in
|
|
69
69
|
// front of it reads two pages of wiring it was told to skip before it reached
|
|
70
70
|
// the first call (round fifteen, entry 1).
|
|
71
|
-
const lines = ["# PlotCoder — for agents", "", AGENTS.lead, "", `The guide: ${AGENTS.guide}. Read it once, before your first call if you can; it is the whole and this page is its first page, and where the two differ, the guide wins. Then Call these first, below
|
|
71
|
+
const lines = ["# PlotCoder — for agents", "", AGENTS.lead, "", `The guide: ${AGENTS.guide}. Read it once, before your first call if you can; it is the whole and this page is its first page, and where the two differ, the guide wins. Then Call these first, below.`, "", "## Call these first"];
|
|
72
72
|
AGENTS.first.forEach((item, index) => lines.push(`${index + 1}. ${item.tool} — ${item.why}`));
|
|
73
73
|
lines.push("", AGENTS.firstNote);
|
|
74
74
|
lines.push("", "## Rules");
|
package/src/board/readWall.js
CHANGED
|
@@ -533,8 +533,11 @@ export function readWall(state, options = {}) {
|
|
|
533
533
|
// reading does, on every read.
|
|
534
534
|
const left = [];
|
|
535
535
|
// A question whose every card is open is not asked (R59): the writer's word covers it.
|
|
536
|
-
//
|
|
537
|
-
|
|
536
|
+
// Not so for a question about the story around the card: a thread's loose end
|
|
537
|
+
// (R60) is the writer's own claim, and the run's questions — beats back to
|
|
538
|
+
// back, a sag — are about the cards between, as the guide promises (round
|
|
539
|
+
// nineteen, entry 28). Those are asked whether or not the cards are open.
|
|
540
|
+
const openCards = openIds.size ? findings.filter((finding) => !ASKED_OF_OPEN_CARDS.has(finding.kind) && finding.ids.some((id) => byId.has(id)) && finding.ids.filter((id) => byId.has(id)).every((id) => openIds.has(id))) : [];
|
|
538
541
|
const asked = findings.filter((finding) => {
|
|
539
542
|
if (openCards.includes(finding)) return false;
|
|
540
543
|
const entry = (state.left ?? []).find(
|
|
@@ -565,13 +568,19 @@ export function readWall(state, options = {}) {
|
|
|
565
568
|
* closed (round eighteen, entry 27): the reading of the same wall with the
|
|
566
569
|
* words cleared, read once more, so the writer can see what the words hide.
|
|
567
570
|
*/
|
|
571
|
+
/** The question kinds an open card does not silence: about the story around it, not the card (R59, R60). */
|
|
572
|
+
const ASKED_OF_OPEN_CARDS = new Set(["loose", "empty", "sag"]);
|
|
573
|
+
|
|
568
574
|
function describeOpen(state, options, order, openIds) {
|
|
569
575
|
if (!openIds.size) return [];
|
|
570
576
|
const hides = new Map();
|
|
571
577
|
if (!options.closedReading) {
|
|
572
578
|
const closed = { ...state, notes: state.notes.map((note) => (openIds.has(note.id) ? { ...note, open: "" } : note)) };
|
|
573
579
|
const again = readWall(closed, { ...options, closedReading: true });
|
|
574
|
-
|
|
580
|
+
// A question about the story around the card is asked whether or not the
|
|
581
|
+
// card is open, so it is never something the open words hide (round
|
|
582
|
+
// nineteen, entries 23 and 28).
|
|
583
|
+
for (const finding of again.findings) if (!ASKED_OF_OPEN_CARDS.has(finding.kind)) for (const id of finding.ids) if (openIds.has(id)) (hides.get(id) ?? hides.set(id, new Set()).get(id)).add(finding.kind);
|
|
575
584
|
}
|
|
576
585
|
return order.filter((note) => openIds.has(note.id)).map((note) => ({ id: note.id, words: note.open.trim(), hides: [...(hides.get(note.id) ?? [])] }));
|
|
577
586
|
}
|
package/src/board/workflows.js
CHANGED
|
@@ -25,7 +25,7 @@ export const WORKFLOWS = [
|
|
|
25
25
|
needs: [
|
|
26
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
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,
|
|
28
|
+
{ question: "Which scenes are the turns?", hint: "Name them, say \"propose them and I will strike\", or say \"mark none yet\" — every card stays a scene and the reading asks once for a beat until you do.", 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
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" },
|