plotcoder-board 0.1.0

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.
@@ -0,0 +1,24 @@
1
+ // Type surface for workflows.js — workflows (R27) and the brief (R28).
2
+
3
+ import type { BoardState } from "./reducer";
4
+
5
+ export type Workflow = {
6
+ id: string;
7
+ name: string;
8
+ /** What the writer says to their agent, verbatim or nearly. */
9
+ ask: string;
10
+ /** The tools the agent will compose. */
11
+ tools: string[];
12
+ /** The rule the agent keeps while doing it. */
13
+ then: string;
14
+ };
15
+
16
+ export declare const WORKFLOWS: readonly Workflow[];
17
+ export declare function workflowById(id: string): Workflow | null;
18
+
19
+ /** The brief for one card, or several in order. Null when none of the ids is a card. */
20
+ export declare function segmentBrief(
21
+ state: BoardState,
22
+ ids: string[],
23
+ options?: { title?: string },
24
+ ): string | null;
@@ -0,0 +1,116 @@
1
+ // Workflows (R27, closing open question 24) and the first step toward the
2
+ // horizon (R28): the brief.
3
+ //
4
+ // A workflow is a named sequence of tool calls a writer launches as one act.
5
+ // The decision recorded here: a workflow is a **prompt in the repo** — the
6
+ // agent interprets it with the tools it already has — and where the sequence
7
+ // is fixed and needs no judgement it is a tool instead (read_wall, organize,
8
+ // apply_template, export_fountain already are). Nothing is stored in the
9
+ // project; a writer says the workflow's name to their agent, and the app
10
+ // shows the list so they know what they can ask for. DOM-free, shared by the
11
+ // app, the skill and the MCP server.
12
+
13
+ export const WORKFLOWS = [
14
+ {
15
+ id: "break-a-treatment",
16
+ name: "Break a treatment into a wall",
17
+ ask: "Here is a treatment. Break it into a wall: one card per scene with a headline and what changes, the cast on each card, the places, and the major turns marked as beats.",
18
+ tools: ["list_words", "read_wall", "list_reminders", "new_board", "rename_project", "set_target", "create_note", "list_board", "add_character", "cast", "update_character", "set_location", "set_rank", "set_length", "set_plant", "create_arrow", "create_group", "organize"],
19
+ then: "Read the wall (read_wall) and say what it asks. A treatment is cards, one create_note each, with characters, location, rank and plants on the call; import_fountain is the door for pages, not a treatment — a scene's text measures its card.",
20
+ },
21
+ {
22
+ id: "read-and-raise",
23
+ name: "Read the wall and raise questions",
24
+ ask: "Read the wall and tell me what it asks: the sagging run, the missing setup, the person who disappears, the two scenes doing one job. Change nothing.",
25
+ tools: ["read_wall", "list_board", "list_reminders"],
26
+ then: "Wait for the writer; propose, do not fix.",
27
+ },
28
+ {
29
+ id: "lay-a-structure",
30
+ name: "Lay a structure over what is here",
31
+ ask: "Lay the turns structure on this wall, then move my scenes under the beats they belong to and organize the rows.",
32
+ tools: ["apply_template", "list_board", "move_note", "organize"],
33
+ then: "Say which scenes you could not place.",
34
+ },
35
+ {
36
+ id: "draft-a-sequence",
37
+ name: "Draft a sequence in Fountain from its cards",
38
+ ask: "Draft the scenes between the second and third beats as Fountain, from their cards, cast pages and places, and write each one onto its card.",
39
+ tools: ["read_pages", "list_board", "write_scene", "export_fountain"],
40
+ then: "Keep to the change line of each card; do not add scenes.",
41
+ },
42
+ {
43
+ id: "restick",
44
+ name: "Restick the remaining cards after the pages moved",
45
+ ask: "The pages changed the story. Read the pages, tell me which cards no longer say what their scenes do, and rewrite those headlines and change lines to match.",
46
+ tools: ["read_pages", "update_note", "read_wall"],
47
+ then: "Never touch a card whose scene is unwritten.",
48
+ },
49
+ {
50
+ id: "brief-a-segment",
51
+ name: "Brief a segment for video",
52
+ ask: "Brief the scene on this card for a video tool: who is in it and what they look and sound like, where it is, what happens, and what must be true after it.",
53
+ tools: ["segment_brief", "build_segment", "add_take", "list_takes", "read_pages", "list_board"],
54
+ then: "Hand the brief to the writer to approve before any tool makes anything; file what is made with add_take.",
55
+ },
56
+ ];
57
+
58
+ export function workflowById(id) {
59
+ return WORKFLOWS.find((workflow) => workflow.id === id) ?? null;
60
+ }
61
+
62
+ // --- The brief (R28, first step; closing open question 25 for now) ----------
63
+ //
64
+ // A segment is a card — one scene — by default, and the run between two
65
+ // beats when the writer asks for a sequence. The brief is text: everything
66
+ // the wall knows about the segment, in the order a video tool would need it.
67
+ // No provider is named (question 26): the brief is what any of them is
68
+ // handed.
69
+
70
+ function personLine(character) {
71
+ const lines = [];
72
+ if (character.looks) lines.push(`looks: ${character.looks}`);
73
+ if (character.voice) lines.push(`voice: ${character.voice}`);
74
+ if (character.wants) lines.push(`wants: ${character.wants}`);
75
+ if (character.needs) lines.push(`needs: ${character.needs}`);
76
+ return `${character.name}${lines.length ? ` — ${lines.join("; ")}` : " — (no page yet)"}`;
77
+ }
78
+
79
+ /**
80
+ * The brief for one card, or for several in order (a run between beats).
81
+ * Returns plain text with headed lines, and nothing the wall does not hold.
82
+ */
83
+ export function segmentBrief(state, ids, options = {}) {
84
+ const byId = new Map(state.notes.map((note) => [note.id, note]));
85
+ const notes = ids.map((id) => byId.get(id)).filter(Boolean);
86
+ if (notes.length === 0) return null;
87
+ const people = new Map();
88
+ for (const note of notes) {
89
+ for (const id of note.characterIds ?? []) {
90
+ const character = state.characters.find((item) => item.id === id);
91
+ if (character) people.set(id, character);
92
+ }
93
+ }
94
+ const places = [...new Set(notes.map((note) => note.location).filter(Boolean))];
95
+ const lines = [];
96
+ lines.push(`SEGMENT: ${notes.length === 1 ? notes[0].headline : `${notes.length} scenes, from "${notes[0].headline}" to "${notes.at(-1).headline}"`}`);
97
+ if (options.title) lines.push(`FROM: ${options.title}`);
98
+ if (state.logline) lines.push(`STORY: ${state.logline}`);
99
+ lines.push(`PEOPLE: ${people.size ? [...people.values()].map(personLine).join(" | ") : "(nobody cast)"}`);
100
+ lines.push(`PLACES: ${places.length ? places.join("; ") : "(none set)"}`);
101
+ for (const note of notes) {
102
+ lines.push("");
103
+ lines.push(`SCENE: ${note.headline}${note.location ? ` — at ${note.location}` : ""}`);
104
+ lines.push(`WHAT CHANGES: ${note.change}`);
105
+ if (note.plants) lines.push("PLANTS: something here pays off later; keep it visible.");
106
+ if (note.text && note.text.trim()) {
107
+ lines.push("SCRIPT:");
108
+ lines.push(note.text.trim());
109
+ } else {
110
+ lines.push("SCRIPT: (unwritten — build from the change line)");
111
+ }
112
+ }
113
+ lines.push("");
114
+ lines.push(`AFTER: ${notes.at(-1).change}`);
115
+ return lines.join("\n");
116
+ }
@@ -0,0 +1,5 @@
1
+ // Type surface for zip.js — a stored zip writer (Roadmap 2, item 5).
2
+
3
+ export declare function crc32(bytes: Uint8Array): number;
4
+ export declare function zip(entries: Array<{ name: string; bytes: Uint8Array | ArrayBuffer; date?: Date | string }>, now?: Date): Uint8Array;
5
+ export declare function unzip(bytes: Uint8Array): Array<{ name: string; bytes: Uint8Array }>;
@@ -0,0 +1,134 @@
1
+ // A zip writer (Roadmap 2, item 5): every file of a person or a project as
2
+ // one package. Stored, not deflated — pictures and video are already
3
+ // compressed — with a CRC-32 per entry as the format requires. Pure and
4
+ // DOM-free: bytes in, bytes out, so it is tested like the kernel and works
5
+ // in Node as in the browser.
6
+
7
+ const CRC_TABLE = (() => {
8
+ const table = new Uint32Array(256);
9
+ for (let n = 0; n < 256; n += 1) {
10
+ let c = n;
11
+ for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
12
+ table[n] = c >>> 0;
13
+ }
14
+ return table;
15
+ })();
16
+
17
+ export function crc32(bytes) {
18
+ let crc = 0xffffffff;
19
+ for (let i = 0; i < bytes.length; i += 1) crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
20
+ return (crc ^ 0xffffffff) >>> 0;
21
+ }
22
+
23
+ function dosDateTime(date) {
24
+ const d = date instanceof Date ? date : new Date(date);
25
+ const year = Math.max(1980, d.getFullYear());
26
+ const time = (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2);
27
+ const day = ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
28
+ return { time, day };
29
+ }
30
+
31
+ function u16(view, at, value) {
32
+ view.setUint16(at, value & 0xffff, true);
33
+ }
34
+
35
+ function u32(view, at, value) {
36
+ view.setUint32(at, value >>> 0, true);
37
+ }
38
+
39
+ /**
40
+ * Entries are { name, bytes: Uint8Array, date? }. Names are UTF-8 paths with
41
+ * forward slashes. Returns the zip as a Uint8Array.
42
+ */
43
+ export function zip(entries, now = new Date()) {
44
+ const encoder = new TextEncoder();
45
+ const locals = [];
46
+ const centrals = [];
47
+ let offset = 0;
48
+ for (const entry of entries) {
49
+ const name = encoder.encode(entry.name.replace(/\\/g, "/"));
50
+ const bytes = entry.bytes instanceof Uint8Array ? entry.bytes : new Uint8Array(entry.bytes);
51
+ const crc = crc32(bytes);
52
+ const { time, day } = dosDateTime(entry.date ?? now);
53
+
54
+ const local = new Uint8Array(30 + name.length);
55
+ const lv = new DataView(local.buffer);
56
+ u32(lv, 0, 0x04034b50);
57
+ u16(lv, 4, 20); // version needed
58
+ u16(lv, 6, 0x0800); // flags: UTF-8 names
59
+ u16(lv, 8, 0); // stored
60
+ u16(lv, 10, time);
61
+ u16(lv, 12, day);
62
+ u32(lv, 14, crc);
63
+ u32(lv, 18, bytes.length);
64
+ u32(lv, 22, bytes.length);
65
+ u16(lv, 26, name.length);
66
+ u16(lv, 28, 0);
67
+ local.set(name, 30);
68
+ locals.push(local, bytes);
69
+
70
+ const central = new Uint8Array(46 + name.length);
71
+ const cv = new DataView(central.buffer);
72
+ u32(cv, 0, 0x02014b50);
73
+ u16(cv, 4, 20); // made by
74
+ u16(cv, 6, 20); // needed
75
+ u16(cv, 8, 0x0800);
76
+ u16(cv, 10, 0);
77
+ u16(cv, 12, time);
78
+ u16(cv, 14, day);
79
+ u32(cv, 16, crc);
80
+ u32(cv, 20, bytes.length);
81
+ u32(cv, 24, bytes.length);
82
+ u16(cv, 28, name.length);
83
+ u16(cv, 30, 0);
84
+ u16(cv, 32, 0);
85
+ u16(cv, 34, 0);
86
+ u16(cv, 36, 0);
87
+ u32(cv, 38, 0);
88
+ u32(cv, 42, offset);
89
+ central.set(name, 46);
90
+ centrals.push(central);
91
+
92
+ offset += local.length + bytes.length;
93
+ }
94
+ const centralSize = centrals.reduce((sum, part) => sum + part.length, 0);
95
+ const end = new Uint8Array(22);
96
+ const ev = new DataView(end.buffer);
97
+ u32(ev, 0, 0x06054b50);
98
+ u16(ev, 4, 0);
99
+ u16(ev, 6, 0);
100
+ u16(ev, 8, entries.length);
101
+ u16(ev, 10, entries.length);
102
+ u32(ev, 12, centralSize);
103
+ u32(ev, 16, offset);
104
+ u16(ev, 20, 0);
105
+
106
+ const total = offset + centralSize + end.length;
107
+ const out = new Uint8Array(total);
108
+ let at = 0;
109
+ for (const part of [...locals, ...centrals, end]) {
110
+ out.set(part, at);
111
+ at += part.length;
112
+ }
113
+ return out;
114
+ }
115
+
116
+ /** The entries of a zip this module wrote (or any stored zip): { name, bytes }. For tests and round trips. */
117
+ export function unzip(bytes) {
118
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
119
+ const decoder = new TextDecoder();
120
+ const entries = [];
121
+ let at = 0;
122
+ while (at + 30 <= bytes.length && view.getUint32(at, true) === 0x04034b50) {
123
+ const method = view.getUint16(at + 8, true);
124
+ const size = view.getUint32(at + 18, true);
125
+ const nameLength = view.getUint16(at + 26, true);
126
+ const extraLength = view.getUint16(at + 28, true);
127
+ const name = decoder.decode(bytes.subarray(at + 30, at + 30 + nameLength));
128
+ const start = at + 30 + nameLength + extraLength;
129
+ if (method !== 0) throw new Error(`unzip: ${name} is not stored`);
130
+ entries.push({ name, bytes: bytes.subarray(start, start + size) });
131
+ at = start + size;
132
+ }
133
+ return entries;
134
+ }