plotcoder-board 0.1.5 → 0.1.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotcoder-board",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -790,6 +790,15 @@ function summarize(state) {
790
790
  return ` - ${character.id} — "${character.name}" on ${on} card${on === 1 ? "" : "s"}${brief}`;
791
791
  })
792
792
  .join("\n");
793
+ const placeCounts = new Map();
794
+ for (const note of state.notes) {
795
+ const phrase = (note.location ?? "").trim();
796
+ if (phrase) placeCounts.set(phrase, (placeCounts.get(phrase) ?? 0) + 1);
797
+ }
798
+ const places = [...placeCounts.entries()]
799
+ .sort((a, b) => a[0].localeCompare(b[0]))
800
+ .map(([phrase, count]) => ` - "${phrase}" on ${count} card${count === 1 ? "" : "s"}`)
801
+ .join("\n");
793
802
  const { beats, scenes } = countRanks(state);
794
803
  const headline = (id) =>
795
804
  state.notes.find((note) => note.id === id)?.headline ?? "(missing card)";
@@ -823,10 +832,14 @@ function summarize(state) {
823
832
  `logline: ${state.logline ? `"${state.logline}"` : "(not set)"}`,
824
833
  ...production,
825
834
  `beats: ${beats}, scenes: ${scenes}`,
826
- `runtime: about ${formatPages(runtime)} pages of a ${formatPages(state.targetEighths)}-page target — ${over > 0 ? `${formatPages(over)} over` : over < 0 ? `${formatPages(-over)} under` : "on it"} (an estimate from the cards; a page runs about a minute)${state.targetEighths === DEFAULT_TARGET_EIGHTHS ? " — the target is the feature default, nobody's choice yet; set_target for a pilot or a half-hour" : ""}`,
835
+ state.targetEighths === DEFAULT_TARGET_EIGHTHS
836
+ ? `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`
837
+ : `runtime: about ${formatPages(runtime)} pages of a ${formatPages(state.targetEighths)}-page target — ${over > 0 ? `${formatPages(over)} over` : over < 0 ? `${formatPages(-over)} under` : "on it"} (an estimate from the cards; a page runs about a minute)`,
827
838
  `notes: ${state.notes.length}, groups: ${state.groups.length}, arrows: ${state.arrows.length}, cast: ${state.characters.length}`,
828
839
  "cast:",
829
840
  cast || " (no one yet — add_character to start the roster)",
841
+ "places (each distinct phrase is one place; near-matches sit side by side):",
842
+ places || " (no card says where it happens yet)",
830
843
  "cards:",
831
844
  notes || " (no cards)",
832
845
  "groups:",
@@ -996,7 +1009,7 @@ server.registerTool(
996
1009
  },
997
1010
  },
998
1011
  async (args) => {
999
- const { result, live } = await commit({
1012
+ let { result, live } = await commit({
1000
1013
  type: "create_note",
1001
1014
  headline: args.headline,
1002
1015
  change: args.change,
@@ -1027,7 +1040,12 @@ server.registerTool(
1027
1040
  }
1028
1041
  if (person) ids.push(person.id);
1029
1042
  }
1030
- if (ids.length) await commit({ type: "set_cast", ids: [result.id], characterIds: ids });
1043
+ if (ids.length) {
1044
+ const cast = await commit({ type: "set_cast", ids: [result.id], characterIds: ids });
1045
+ // The card as it is now, cast and all, so the reply's JSON agrees with its prose.
1046
+ const after = cast.state.notes.find((note) => note.id === result.id);
1047
+ if (after) result = after;
1048
+ }
1031
1049
  castLine = ` Cast: ${args.characters.map((name) => name.trim()).join(", ")}${added.length ? ` (added to the roster: ${added.join(", ")})` : ""}.`;
1032
1050
  }
1033
1051
  const landed = [
@@ -1194,7 +1212,7 @@ server.registerTool(
1194
1212
  }
1195
1213
  const shape = beats
1196
1214
  ? `${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}"`).join(", ")}` : ""}`
1197
- : `${rows} row(s)`;
1215
+ : `${rows} row(s) five cards wide — no beats yet, so nothing sets the rows; set_rank the turns and organize again for a row per beat`;
1198
1216
  return ok(`Organized ${poses.length} card(s) along the arrows into ${shape}${where(live)}.`, poses);
1199
1217
  },
1200
1218
  );
@@ -1927,8 +1945,16 @@ server.registerTool(
1927
1945
  return ok("No place changed: those cards already read that way.");
1928
1946
  }
1929
1947
  const place = result[0].location;
1948
+ // A near match on the wall is probably the same place spelled twice.
1949
+ const wanted = (place ?? "").trim().toLowerCase();
1950
+ const near = wanted
1951
+ ? [...new Set(state.notes.map((note) => (note.location ?? "").trim()).filter(Boolean))].filter(
1952
+ (other) => other.toLowerCase() !== wanted && (other.toLowerCase().includes(wanted) || wanted.includes(other.toLowerCase())),
1953
+ )
1954
+ : [];
1955
+ 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.` : "";
1930
1956
  return ok(
1931
- `${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.`,
1957
+ `${result.length} card(s) now ${place ? `at ${place}` : "nowhere"}${where(live)}.${warn}`,
1932
1958
  result,
1933
1959
  );
1934
1960
  },
@@ -2074,7 +2100,7 @@ server.registerTool(
2074
2100
  {
2075
2101
  title: "Create arrow",
2076
2102
  description:
2077
- "Draw a directed arrow from one card to another. kind 'follows' (the default) says what comes after what; kind 'setup' says the first card plants something the second pays off. Arrows are one-way: A→B does not create B→A. If you want both, call this twice — that is two arrows, not one two-headed line. A card cannot point at itself, and the same direction cannot be drawn twice, whatever its kind; use set_arrow_kind to change one.",
2103
+ "Draw a directed arrow from one card to another. kind 'follows' (the default) says what comes after what — a straight sequence needs them too: organize lays the wall out along them, and the wall asks about a card no arrow touches; kind 'setup' says the first card plants something the second pays off. Arrows are one-way: A→B does not create B→A. If you want both, call this twice — that is two arrows, not one two-headed line. A card cannot point at itself, and the same direction cannot be drawn twice, whatever its kind; use set_arrow_kind to change one.",
2078
2104
  inputSchema: { from: z.string(), to: z.string(), kind: arrowKindSchema.optional() },
2079
2105
  },
2080
2106
  async (args) => {
@@ -2624,7 +2650,7 @@ server.registerTool(
2624
2650
  const next = renameBoard(project, target.id, args.name);
2625
2651
  if (next === project) return ok(`"${target.name}" already has that name.`);
2626
2652
  const live = await writeProject(next, boards, rev, base);
2627
- return ok(`Renamed to "${args.name.trim()}"${where(live)}.`);
2653
+ return ok(`Renamed board "${target.name}" (${target.id}) to "${args.name.trim()}"${where(live)}.`, { id: target.id, name: args.name.trim() });
2628
2654
  },
2629
2655
  );
2630
2656
 
@@ -59,7 +59,8 @@ export type WallReading = {
59
59
  runs: Run[];
60
60
  setups: Setup[];
61
61
  /** Every planted card: the scene that pays it off (first setup arrow, by wall order), or null while unpaid. */
62
- payoffs: Record<string, string | null>;
62
+ /** For each folded card, the cards its setup arrows land on, in wall order; empty when unpaid. */
63
+ payoffs: Record<string, string[]>;
63
64
  findings: Finding[];
64
65
  };
65
66
 
@@ -303,7 +303,9 @@ export function readWall(state) {
303
303
  .filter((arrow) => arrow.kind === "setup" && arrow.from === note.id)
304
304
  .map((arrow) => arrow.to)
305
305
  .sort((a, b) => (wallIndex.get(a) ?? Infinity) - (wallIndex.get(b) ?? Infinity));
306
- payoffs[note.id] = heads[0] ?? null;
306
+ // Every payoff, in wall order: a card can plant two things (the ledger
307
+ // pays off at the cash and again at the initials), and both count.
308
+ payoffs[note.id] = heads;
307
309
  }
308
310
  for (const note of order) {
309
311
  if (note.plants && !paysOff.has(note.id)) {