@jokerized/decksmith 0.9.6 → 0.9.8

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/dist/cli.js CHANGED
@@ -23530,9 +23530,9 @@ function literalFindings(storyboard, source, opts = {}) {
23530
23530
  prevKey = { id: beat2.id, key };
23531
23531
  });
23532
23532
  const tables = beats.filter((b) => b.literal?.kind === "table");
23533
- if (tables.length > 1)
23533
+ for (const t2 of tables.slice(1))
23534
23534
  out.push(
23535
- `The plan draws ${tables.length} tables (${tables.map((b) => b.id).join(", ")}). A table reads like a report: keep at most one, and draw the others as what they list \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
23535
+ `${t2.id} draws a table, and so does ${tables[0]?.id}. A table reads like a report: keep at most one, and draw this one as what it lists \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
23536
23536
  );
23537
23537
  const byVectors = /* @__PURE__ */ new Map();
23538
23538
  for (const b of beats) {
@@ -23541,20 +23541,20 @@ function literalFindings(storyboard, source, opts = {}) {
23541
23541
  const key = JSON.stringify(l.vectors.map((v) => v.values));
23542
23542
  byVectors.set(key, [...byVectors.get(key) ?? [], b.id]);
23543
23543
  }
23544
- for (const ids of byVectors.values())
23545
- if (ids.length > 1)
23544
+ for (const [first, ...rest] of byVectors.values())
23545
+ for (const id2 of rest)
23546
23546
  out.push(
23547
- `${ids.join(" and ")} run operate on the same vectors, so the deck shows one chart twice. Give the second beat's chain to the first (one longer chain), or draw the second another way.`
23547
+ `${id2} runs operate on the same vectors as ${first}, so the deck shows one chart twice. Give its chain to ${first} (one longer chain), or draw ${id2} another way.`
23548
23548
  );
23549
23549
  const forms = /* @__PURE__ */ new Map();
23550
23550
  for (const b of beats) {
23551
23551
  const f = b.literal && wordForm(b.literal);
23552
23552
  if (f) forms.set(f, [...forms.get(f) ?? [], b.id]);
23553
23553
  }
23554
- for (const [f, ids] of forms)
23555
- if (ids.length > 1)
23554
+ for (const [f, [first, ...rest]] of forms)
23555
+ for (const id2 of rest)
23556
23556
  out.push(
23557
- `The plan draws \`${f}\` ${ids.length} times (${ids.join(", ")}). It draws no picture, so a second one is the same slide again: keep it on one beat, and draw the others another way or with \`literal\` null.`
23557
+ `${id2} draws \`${f}\`, as ${first} already does. It draws no picture, so a second one is the same slide again: draw ${id2} another way, or with \`literal\` null.`
23558
23558
  );
23559
23559
  return out;
23560
23560
  }
@@ -23630,15 +23630,26 @@ function isScene(beat2, design2) {
23630
23630
  return pictured(p.backdrop);
23631
23631
  }
23632
23632
  }
23633
+ function shapesFor(storyboard, i, offered, images) {
23634
+ const beats = storyboard.beats;
23635
+ const others = beats.filter((_, k) => k !== i);
23636
+ const kinetic2 = others.filter((b) => b.archetype === "kinetic").length;
23637
+ const panels = others.filter((b) => PANEL_ARCHETYPES.has(b.archetype)).length;
23638
+ const panelCap = panelsAllowed(beats.length, images);
23639
+ return offered.filter(
23640
+ (a) => a !== beats[i - 1]?.archetype && a !== beats[i + 1]?.archetype && (a !== "title" || i === 0) && (a !== "kinetic" || kinetic2 < KINETIC_MAX) && (!PANEL_ARCHETYPES.has(a) || panels < panelCap)
23641
+ );
23642
+ }
23633
23643
  function varietyFindings(storyboard, images, design2) {
23634
23644
  const beats = storyboard.beats;
23635
23645
  const out = [];
23646
+ const avoid = (beat2, other) => [beat2.archetype, other?.archetype].filter((x, k, xs) => x !== void 0 && xs.indexOf(x) === k).map((x) => `\`${x}\``).join(" or ");
23636
23647
  for (let i = 1; i < beats.length; i++) {
23637
23648
  const a = beats[i - 1];
23638
23649
  const b = beats[i];
23639
23650
  if (a.archetype === b.archetype) {
23640
23651
  out.push(
23641
- `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Draw one of them as a different shape.`
23652
+ `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Redraw ${a.id} as anything but ${avoid(a, beats[i - 2])}, or ${b.id} as anything but ${avoid(b, beats[i + 1])}.`
23642
23653
  );
23643
23654
  }
23644
23655
  }
@@ -24997,13 +25008,39 @@ function operateVariants(v) {
24997
25008
  return [only(OPERATE_CHAIN, OPERATE_TERMS), only(OPERATE_TERMS)];
24998
25009
  }
24999
25010
  function schemaFor(prefs, source) {
25000
- return splitOperate(
25001
- hideFromPlanner(
25002
- forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
25003
- plannerInvisible(prefs, source)
25011
+ return shareLiteral(
25012
+ splitOperate(
25013
+ hideFromPlanner(
25014
+ forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
25015
+ plannerInvisible(prefs, source)
25016
+ )
25004
25017
  )
25005
25018
  );
25006
25019
  }
25020
+ function shareLiteral(schema) {
25021
+ const root = schema;
25022
+ const variants = root.properties.beats.items.anyOf;
25023
+ const literal = variants[0]?.properties.literal;
25024
+ if (!literal) return schema;
25025
+ const one2 = JSON.stringify(literal);
25026
+ if (!variants.every((v) => JSON.stringify(v.properties.literal) === one2))
25027
+ throw new Error("schemaFor: the beat variants carry different `literal` schemas.");
25028
+ const ref = { $ref: "#/$defs/literal" };
25029
+ return {
25030
+ ...root,
25031
+ $defs: { literal },
25032
+ properties: {
25033
+ ...root.properties,
25034
+ beats: {
25035
+ ...root.properties.beats,
25036
+ items: {
25037
+ ...root.properties.beats.items,
25038
+ anyOf: variants.map((v) => ({ ...v, properties: { ...v.properties, literal: ref } }))
25039
+ }
25040
+ }
25041
+ }
25042
+ };
25043
+ }
25007
25044
  var SCHEMA = schemaFor(prefsSchema.parse({}));
25008
25045
  async function codexPlanner(source, opts = {}) {
25009
25046
  const prefs = opts.prefs ?? prefsSchema.parse({});
@@ -25011,7 +25048,8 @@ async function codexPlanner(source, opts = {}) {
25011
25048
  try {
25012
25049
  const schemaPath = join9(dir, "storyboard.schema.json");
25013
25050
  const outPath = join9(dir, "storyboard.json");
25014
- await writeFile7(schemaPath, JSON.stringify(schemaFor(prefs, source)));
25051
+ const schema = schemaFor(prefs, source);
25052
+ await writeFile7(schemaPath, JSON.stringify(schema));
25015
25053
  const ask = async (prompt2) => {
25016
25054
  await rm3(outPath, { force: true });
25017
25055
  await (opts.run ?? runCodex)({
@@ -25155,6 +25193,46 @@ ${left.map((m) => ` ${m}`).join("\n")}`
25155
25193
  );
25156
25194
  return plan;
25157
25195
  };
25196
+ const beatSchemaPath = join9(dir, "beat.schema.json");
25197
+ const offered = beatVariants(schema).map((v) => v.properties.archetype.const);
25198
+ const reshape2 = async (deck, i) => {
25199
+ const beat2 = deck.beats[i];
25200
+ const shapes = shapesFor(deck, i, offered, prefs.images);
25201
+ if (!shapes.length) return deck;
25202
+ await writeFile7(beatSchemaPath, JSON.stringify(beatSchemaFor(schema, shapes)));
25203
+ let problems = [];
25204
+ for (let t2 = 1; t2 <= SCENE_REPAIRS; t2++) {
25205
+ opts.onReshape?.(beat2.id, shapes);
25206
+ const out = join9(dir, `beat-${beat2.id}-${t2}.json`);
25207
+ try {
25208
+ await (opts.run ?? runCodex)({
25209
+ prompt: beatPrompt(prompt, deck, beat2, shapes, problems),
25210
+ schemaPath: beatSchemaPath,
25211
+ outPath: out,
25212
+ ...opts.model === void 0 ? {} : { model: opts.model },
25213
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
25214
+ });
25215
+ const got = stripNulls(JSON.parse(await readFile8(out, "utf8")))?.beat;
25216
+ const redrawn = {
25217
+ ...got,
25218
+ id: beat2.id,
25219
+ ...beat2.literal ? { literal: beat2.literal } : {}
25220
+ };
25221
+ const beats = deck.beats.map((b, k) => k === i ? redrawn : b);
25222
+ return withStockLabels(checkPlan({ ...deck, beats }, source, prefs), source);
25223
+ } catch (err) {
25224
+ problems = err instanceof PlanRejected ? err.problems : [`the ask failed: ${err instanceof Error ? err.message : err}`];
25225
+ }
25226
+ }
25227
+ return deck;
25228
+ };
25229
+ const neighbours = async (planned) => {
25230
+ let plan = planned;
25231
+ for (let i = 1; i < plan.beats.length; i++)
25232
+ if (plan.beats[i]?.archetype === plan.beats[i - 1]?.archetype)
25233
+ plan = await reshape2(plan, i);
25234
+ return plan;
25235
+ };
25158
25236
  let answer = await attempt2(prompt);
25159
25237
  if (answer.plan && listOf(answer.broken).length === 0) return await scenes(answer.plan);
25160
25238
  const firstBeats = beatCount(answer.json);
@@ -25174,7 +25252,13 @@ ${left.map((m) => ` ${m}`).join("\n")}`
25174
25252
  advice: []
25175
25253
  };
25176
25254
  if (next.plan && listOf(still).length === 0) return await scenes(next.plan);
25177
- if (round8 === REPAIRS) throw unrepaired(next, still, round8);
25255
+ if (round8 === REPAIRS) {
25256
+ if (!next.plan || !listOf(still).every(isNeighbour)) throw unrepaired(next, still, round8);
25257
+ const plan = await neighbours(next.plan);
25258
+ const left = { ...findings(plan), advice: [] };
25259
+ if (listOf(left).length) throw unrepaired(next, left, round8);
25260
+ return await scenes(plan);
25261
+ }
25178
25262
  merging ||= still.size.length > 0;
25179
25263
  answer = { ...next, broken: still };
25180
25264
  }
@@ -25229,6 +25313,37 @@ ${list2}
25229
25313
 
25230
25314
  Return {"literal": ...}: beat ${id2}'s scene, redrawn so none of these hold \u2014 every number, word and figure one the source states, every slot its verb needs filled. Keep its verb unless a finding names another. Return a scene: this ask has no way to leave it out. No other beat is yours to change.
25231
25315
 
25316
+ THE STORYBOARD:
25317
+ ${JSON.stringify(plan, null, 2)}`;
25318
+ }
25319
+ function isNeighbour(finding2) {
25320
+ return /^\S+ and \S+ are both `[^`]+`, next to each other\./.test(finding2);
25321
+ }
25322
+ function beatVariants(schema) {
25323
+ return schema.properties.beats.items.anyOf;
25324
+ }
25325
+ function beatSchemaFor(schema, shapes) {
25326
+ const anyOf = beatVariants(schema).filter((v) => shapes.includes(v.properties.archetype.const)).map((v) => hideFromPlanner(v, /* @__PURE__ */ new Set(["literal"])));
25327
+ return {
25328
+ type: "object",
25329
+ properties: { beat: { anyOf } },
25330
+ required: ["beat"],
25331
+ additionalProperties: false
25332
+ };
25333
+ }
25334
+ function beatPrompt(prompt, plan, beat2, shapes, problems) {
25335
+ const refused = problems.length ? `
25336
+ Your last answer was refused for:
25337
+ ${problems.map((m) => ` - ${m}`).join("\n")}
25338
+ ` : "";
25339
+ return `${prompt}
25340
+
25341
+ THE DECK IS PLANNED. ONE BEAT'S SHAPE IS LEFT.
25342
+
25343
+ The storyboard below is settled, but beat ${beat2.id} is \`${beat2.archetype}\` beside a beat of the same shape, and no two beats in a row may share an archetype.
25344
+ ${refused}
25345
+ Return {"beat": ...}: beat ${beat2.id} redrawn as one of ${shapes.map((s) => `\`${s}\``).join(", ")} \u2014 whichever shows its point best \u2014 with the params that shape needs, every number, word and figure one the source states. Keep its id, its point, its evidence, its narration and its part. No other beat is yours to change.
25346
+
25232
25347
  THE STORYBOARD:
25233
25348
  ${JSON.stringify(plan, null, 2)}`;
25234
25349
  }
@@ -25326,7 +25441,9 @@ async function readPlan(outPath, source, prefs) {
25326
25441
  ${raw2.slice(0, 200)}`
25327
25442
  );
25328
25443
  }
25329
- const plan = stripNulls(parsed);
25444
+ return checkPlan(stripNulls(parsed), source, prefs);
25445
+ }
25446
+ function checkPlan(plan, source, prefs) {
25330
25447
  const result = storyboardSchema.safeParse(plan);
25331
25448
  if (!result.success) {
25332
25449
  const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
@@ -38201,6 +38318,7 @@ imageFlags(
38201
38318
  step(`plan: redrawing ${beat2}'s scene on its own \u2014`);
38202
38319
  for (const m of broken) step(`plan: ${m}`);
38203
38320
  },
38321
+ onReshape: (beat2, shapes) => step(`plan: redrawing ${beat2} on its own, as one of: ${shapes.join(", ")}`),
38204
38322
  onDropScenes: (beats, why3) => {
38205
38323
  step(
38206
38324
  `plan: WARNING: ${beats.join(", ")} drawn by their archetype instead; their scenes stay broken \u2014`
package/dist/index.js CHANGED
@@ -25877,9 +25877,9 @@ function literalFindings(storyboard, source, opts = {}) {
25877
25877
  prevKey = { id: beat2.id, key };
25878
25878
  });
25879
25879
  const tables = beats.filter((b) => b.literal?.kind === "table");
25880
- if (tables.length > 1)
25880
+ for (const t2 of tables.slice(1))
25881
25881
  out.push(
25882
- `The plan draws ${tables.length} tables (${tables.map((b) => b.id).join(", ")}). A table reads like a report: keep at most one, and draw the others as what they list \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
25882
+ `${t2.id} draws a table, and so does ${tables[0]?.id}. A table reads like a report: keep at most one, and draw this one as what it lists \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
25883
25883
  );
25884
25884
  const byVectors = /* @__PURE__ */ new Map();
25885
25885
  for (const b of beats) {
@@ -25888,20 +25888,20 @@ function literalFindings(storyboard, source, opts = {}) {
25888
25888
  const key = JSON.stringify(l.vectors.map((v) => v.values));
25889
25889
  byVectors.set(key, [...byVectors.get(key) ?? [], b.id]);
25890
25890
  }
25891
- for (const ids of byVectors.values())
25892
- if (ids.length > 1)
25891
+ for (const [first, ...rest] of byVectors.values())
25892
+ for (const id2 of rest)
25893
25893
  out.push(
25894
- `${ids.join(" and ")} run operate on the same vectors, so the deck shows one chart twice. Give the second beat's chain to the first (one longer chain), or draw the second another way.`
25894
+ `${id2} runs operate on the same vectors as ${first}, so the deck shows one chart twice. Give its chain to ${first} (one longer chain), or draw ${id2} another way.`
25895
25895
  );
25896
25896
  const forms = /* @__PURE__ */ new Map();
25897
25897
  for (const b of beats) {
25898
25898
  const f = b.literal && wordForm(b.literal);
25899
25899
  if (f) forms.set(f, [...forms.get(f) ?? [], b.id]);
25900
25900
  }
25901
- for (const [f, ids] of forms)
25902
- if (ids.length > 1)
25901
+ for (const [f, [first, ...rest]] of forms)
25902
+ for (const id2 of rest)
25903
25903
  out.push(
25904
- `The plan draws \`${f}\` ${ids.length} times (${ids.join(", ")}). It draws no picture, so a second one is the same slide again: keep it on one beat, and draw the others another way or with \`literal\` null.`
25904
+ `${id2} draws \`${f}\`, as ${first} already does. It draws no picture, so a second one is the same slide again: draw ${id2} another way, or with \`literal\` null.`
25905
25905
  );
25906
25906
  return out;
25907
25907
  }
@@ -25977,15 +25977,26 @@ function isScene(beat2, design2) {
25977
25977
  return pictured(p.backdrop);
25978
25978
  }
25979
25979
  }
25980
+ function shapesFor(storyboard, i, offered, images) {
25981
+ const beats = storyboard.beats;
25982
+ const others = beats.filter((_, k) => k !== i);
25983
+ const kinetic2 = others.filter((b) => b.archetype === "kinetic").length;
25984
+ const panels = others.filter((b) => PANEL_ARCHETYPES.has(b.archetype)).length;
25985
+ const panelCap = panelsAllowed(beats.length, images);
25986
+ return offered.filter(
25987
+ (a) => a !== beats[i - 1]?.archetype && a !== beats[i + 1]?.archetype && (a !== "title" || i === 0) && (a !== "kinetic" || kinetic2 < KINETIC_MAX) && (!PANEL_ARCHETYPES.has(a) || panels < panelCap)
25988
+ );
25989
+ }
25980
25990
  function varietyFindings(storyboard, images, design2) {
25981
25991
  const beats = storyboard.beats;
25982
25992
  const out = [];
25993
+ const avoid = (beat2, other) => [beat2.archetype, other?.archetype].filter((x, k, xs) => x !== void 0 && xs.indexOf(x) === k).map((x) => `\`${x}\``).join(" or ");
25983
25994
  for (let i = 1; i < beats.length; i++) {
25984
25995
  const a = beats[i - 1];
25985
25996
  const b = beats[i];
25986
25997
  if (a.archetype === b.archetype) {
25987
25998
  out.push(
25988
- `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Draw one of them as a different shape.`
25999
+ `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Redraw ${a.id} as anything but ${avoid(a, beats[i - 2])}, or ${b.id} as anything but ${avoid(b, beats[i + 1])}.`
25989
26000
  );
25990
26001
  }
25991
26002
  }
@@ -27344,13 +27355,39 @@ function operateVariants(v) {
27344
27355
  return [only(OPERATE_CHAIN, OPERATE_TERMS), only(OPERATE_TERMS)];
27345
27356
  }
27346
27357
  function schemaFor(prefs, source) {
27347
- return splitOperate(
27348
- hideFromPlanner(
27349
- forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
27350
- plannerInvisible(prefs, source)
27358
+ return shareLiteral(
27359
+ splitOperate(
27360
+ hideFromPlanner(
27361
+ forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
27362
+ plannerInvisible(prefs, source)
27363
+ )
27351
27364
  )
27352
27365
  );
27353
27366
  }
27367
+ function shareLiteral(schema) {
27368
+ const root = schema;
27369
+ const variants = root.properties.beats.items.anyOf;
27370
+ const literal = variants[0]?.properties.literal;
27371
+ if (!literal) return schema;
27372
+ const one2 = JSON.stringify(literal);
27373
+ if (!variants.every((v) => JSON.stringify(v.properties.literal) === one2))
27374
+ throw new Error("schemaFor: the beat variants carry different `literal` schemas.");
27375
+ const ref = { $ref: "#/$defs/literal" };
27376
+ return {
27377
+ ...root,
27378
+ $defs: { literal },
27379
+ properties: {
27380
+ ...root.properties,
27381
+ beats: {
27382
+ ...root.properties.beats,
27383
+ items: {
27384
+ ...root.properties.beats.items,
27385
+ anyOf: variants.map((v) => ({ ...v, properties: { ...v.properties, literal: ref } }))
27386
+ }
27387
+ }
27388
+ }
27389
+ };
27390
+ }
27354
27391
  var SCHEMA = schemaFor(prefsSchema.parse({}));
27355
27392
  async function codexPlanner(source, opts = {}) {
27356
27393
  const prefs = opts.prefs ?? prefsSchema.parse({});
@@ -27358,7 +27395,8 @@ async function codexPlanner(source, opts = {}) {
27358
27395
  try {
27359
27396
  const schemaPath = join12(dir, "storyboard.schema.json");
27360
27397
  const outPath = join12(dir, "storyboard.json");
27361
- await writeFile10(schemaPath, JSON.stringify(schemaFor(prefs, source)));
27398
+ const schema = schemaFor(prefs, source);
27399
+ await writeFile10(schemaPath, JSON.stringify(schema));
27362
27400
  const ask = async (prompt2) => {
27363
27401
  await rm5(outPath, { force: true });
27364
27402
  await (opts.run ?? runCodex)({
@@ -27502,6 +27540,46 @@ ${left.map((m) => ` ${m}`).join("\n")}`
27502
27540
  );
27503
27541
  return plan;
27504
27542
  };
27543
+ const beatSchemaPath = join12(dir, "beat.schema.json");
27544
+ const offered = beatVariants(schema).map((v) => v.properties.archetype.const);
27545
+ const reshape2 = async (deck, i) => {
27546
+ const beat2 = deck.beats[i];
27547
+ const shapes = shapesFor(deck, i, offered, prefs.images);
27548
+ if (!shapes.length) return deck;
27549
+ await writeFile10(beatSchemaPath, JSON.stringify(beatSchemaFor(schema, shapes)));
27550
+ let problems = [];
27551
+ for (let t2 = 1; t2 <= SCENE_REPAIRS; t2++) {
27552
+ opts.onReshape?.(beat2.id, shapes);
27553
+ const out = join12(dir, `beat-${beat2.id}-${t2}.json`);
27554
+ try {
27555
+ await (opts.run ?? runCodex)({
27556
+ prompt: beatPrompt(prompt, deck, beat2, shapes, problems),
27557
+ schemaPath: beatSchemaPath,
27558
+ outPath: out,
27559
+ ...opts.model === void 0 ? {} : { model: opts.model },
27560
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
27561
+ });
27562
+ const got = stripNulls(JSON.parse(await readFile12(out, "utf8")))?.beat;
27563
+ const redrawn = {
27564
+ ...got,
27565
+ id: beat2.id,
27566
+ ...beat2.literal ? { literal: beat2.literal } : {}
27567
+ };
27568
+ const beats = deck.beats.map((b, k) => k === i ? redrawn : b);
27569
+ return withStockLabels(checkPlan({ ...deck, beats }, source, prefs), source);
27570
+ } catch (err) {
27571
+ problems = err instanceof PlanRejected ? err.problems : [`the ask failed: ${err instanceof Error ? err.message : err}`];
27572
+ }
27573
+ }
27574
+ return deck;
27575
+ };
27576
+ const neighbours = async (planned) => {
27577
+ let plan = planned;
27578
+ for (let i = 1; i < plan.beats.length; i++)
27579
+ if (plan.beats[i]?.archetype === plan.beats[i - 1]?.archetype)
27580
+ plan = await reshape2(plan, i);
27581
+ return plan;
27582
+ };
27505
27583
  let answer = await attempt2(prompt);
27506
27584
  if (answer.plan && listOf(answer.broken).length === 0) return await scenes(answer.plan);
27507
27585
  const firstBeats = beatCount(answer.json);
@@ -27521,7 +27599,13 @@ ${left.map((m) => ` ${m}`).join("\n")}`
27521
27599
  advice: []
27522
27600
  };
27523
27601
  if (next.plan && listOf(still).length === 0) return await scenes(next.plan);
27524
- if (round8 === REPAIRS) throw unrepaired(next, still, round8);
27602
+ if (round8 === REPAIRS) {
27603
+ if (!next.plan || !listOf(still).every(isNeighbour)) throw unrepaired(next, still, round8);
27604
+ const plan = await neighbours(next.plan);
27605
+ const left = { ...findings(plan), advice: [] };
27606
+ if (listOf(left).length) throw unrepaired(next, left, round8);
27607
+ return await scenes(plan);
27608
+ }
27525
27609
  merging ||= still.size.length > 0;
27526
27610
  answer = { ...next, broken: still };
27527
27611
  }
@@ -27576,6 +27660,37 @@ ${list2}
27576
27660
 
27577
27661
  Return {"literal": ...}: beat ${id2}'s scene, redrawn so none of these hold \u2014 every number, word and figure one the source states, every slot its verb needs filled. Keep its verb unless a finding names another. Return a scene: this ask has no way to leave it out. No other beat is yours to change.
27578
27662
 
27663
+ THE STORYBOARD:
27664
+ ${JSON.stringify(plan, null, 2)}`;
27665
+ }
27666
+ function isNeighbour(finding2) {
27667
+ return /^\S+ and \S+ are both `[^`]+`, next to each other\./.test(finding2);
27668
+ }
27669
+ function beatVariants(schema) {
27670
+ return schema.properties.beats.items.anyOf;
27671
+ }
27672
+ function beatSchemaFor(schema, shapes) {
27673
+ const anyOf = beatVariants(schema).filter((v) => shapes.includes(v.properties.archetype.const)).map((v) => hideFromPlanner(v, /* @__PURE__ */ new Set(["literal"])));
27674
+ return {
27675
+ type: "object",
27676
+ properties: { beat: { anyOf } },
27677
+ required: ["beat"],
27678
+ additionalProperties: false
27679
+ };
27680
+ }
27681
+ function beatPrompt(prompt, plan, beat2, shapes, problems) {
27682
+ const refused = problems.length ? `
27683
+ Your last answer was refused for:
27684
+ ${problems.map((m) => ` - ${m}`).join("\n")}
27685
+ ` : "";
27686
+ return `${prompt}
27687
+
27688
+ THE DECK IS PLANNED. ONE BEAT'S SHAPE IS LEFT.
27689
+
27690
+ The storyboard below is settled, but beat ${beat2.id} is \`${beat2.archetype}\` beside a beat of the same shape, and no two beats in a row may share an archetype.
27691
+ ${refused}
27692
+ Return {"beat": ...}: beat ${beat2.id} redrawn as one of ${shapes.map((s) => `\`${s}\``).join(", ")} \u2014 whichever shows its point best \u2014 with the params that shape needs, every number, word and figure one the source states. Keep its id, its point, its evidence, its narration and its part. No other beat is yours to change.
27693
+
27579
27694
  THE STORYBOARD:
27580
27695
  ${JSON.stringify(plan, null, 2)}`;
27581
27696
  }
@@ -27673,7 +27788,9 @@ async function readPlan(outPath, source, prefs) {
27673
27788
  ${raw2.slice(0, 200)}`
27674
27789
  );
27675
27790
  }
27676
- const plan = stripNulls(parsed);
27791
+ return checkPlan(stripNulls(parsed), source, prefs);
27792
+ }
27793
+ function checkPlan(plan, source, prefs) {
27677
27794
  const result = storyboardSchema.safeParse(plan);
27678
27795
  if (!result.success) {
27679
27796
  const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
package/dist/mcp.js CHANGED
@@ -24389,9 +24389,9 @@ function literalFindings(storyboard, source, opts = {}) {
24389
24389
  prevKey = { id: beat.id, key };
24390
24390
  });
24391
24391
  const tables = beats.filter((b) => b.literal?.kind === "table");
24392
- if (tables.length > 1)
24392
+ for (const t2 of tables.slice(1))
24393
24393
  out.push(
24394
- `The plan draws ${tables.length} tables (${tables.map((b) => b.id).join(", ")}). A table reads like a report: keep at most one, and draw the others as what they list \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
24394
+ `${t2.id} draws a table, and so does ${tables[0]?.id}. A table reads like a report: keep at most one, and draw this one as what it lists \u2014 results as \`place\` (margin or plane), what was and was not evaluated as \`place\` sets, experiments not run as \`place\` combos, earlier approaches as \`prior-work\`, reported costs as \`place\` length \u2014 or leave \`literal\` null where nothing is honest.`
24395
24395
  );
24396
24396
  const byVectors = /* @__PURE__ */ new Map();
24397
24397
  for (const b of beats) {
@@ -24400,20 +24400,20 @@ function literalFindings(storyboard, source, opts = {}) {
24400
24400
  const key = JSON.stringify(l.vectors.map((v) => v.values));
24401
24401
  byVectors.set(key, [...byVectors.get(key) ?? [], b.id]);
24402
24402
  }
24403
- for (const ids of byVectors.values())
24404
- if (ids.length > 1)
24403
+ for (const [first, ...rest] of byVectors.values())
24404
+ for (const id2 of rest)
24405
24405
  out.push(
24406
- `${ids.join(" and ")} run operate on the same vectors, so the deck shows one chart twice. Give the second beat's chain to the first (one longer chain), or draw the second another way.`
24406
+ `${id2} runs operate on the same vectors as ${first}, so the deck shows one chart twice. Give its chain to ${first} (one longer chain), or draw ${id2} another way.`
24407
24407
  );
24408
24408
  const forms = /* @__PURE__ */ new Map();
24409
24409
  for (const b of beats) {
24410
24410
  const f = b.literal && wordForm(b.literal);
24411
24411
  if (f) forms.set(f, [...forms.get(f) ?? [], b.id]);
24412
24412
  }
24413
- for (const [f, ids] of forms)
24414
- if (ids.length > 1)
24413
+ for (const [f, [first, ...rest]] of forms)
24414
+ for (const id2 of rest)
24415
24415
  out.push(
24416
- `The plan draws \`${f}\` ${ids.length} times (${ids.join(", ")}). It draws no picture, so a second one is the same slide again: keep it on one beat, and draw the others another way or with \`literal\` null.`
24416
+ `${id2} draws \`${f}\`, as ${first} already does. It draws no picture, so a second one is the same slide again: draw ${id2} another way, or with \`literal\` null.`
24417
24417
  );
24418
24418
  return out;
24419
24419
  }
@@ -24489,15 +24489,26 @@ function isScene(beat, design2) {
24489
24489
  return pictured(p.backdrop);
24490
24490
  }
24491
24491
  }
24492
+ function shapesFor(storyboard, i, offered, images) {
24493
+ const beats = storyboard.beats;
24494
+ const others = beats.filter((_, k) => k !== i);
24495
+ const kinetic2 = others.filter((b) => b.archetype === "kinetic").length;
24496
+ const panels = others.filter((b) => PANEL_ARCHETYPES.has(b.archetype)).length;
24497
+ const panelCap = panelsAllowed(beats.length, images);
24498
+ return offered.filter(
24499
+ (a) => a !== beats[i - 1]?.archetype && a !== beats[i + 1]?.archetype && (a !== "title" || i === 0) && (a !== "kinetic" || kinetic2 < KINETIC_MAX) && (!PANEL_ARCHETYPES.has(a) || panels < panelCap)
24500
+ );
24501
+ }
24492
24502
  function varietyFindings(storyboard, images, design2) {
24493
24503
  const beats = storyboard.beats;
24494
24504
  const out = [];
24505
+ const avoid = (beat, other) => [beat.archetype, other?.archetype].filter((x, k, xs) => x !== void 0 && xs.indexOf(x) === k).map((x) => `\`${x}\``).join(" or ");
24495
24506
  for (let i = 1; i < beats.length; i++) {
24496
24507
  const a = beats[i - 1];
24497
24508
  const b = beats[i];
24498
24509
  if (a.archetype === b.archetype) {
24499
24510
  out.push(
24500
- `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Draw one of them as a different shape.`
24511
+ `${a.id} and ${b.id} are both \`${a.archetype}\`, next to each other. Redraw ${a.id} as anything but ${avoid(a, beats[i - 2])}, or ${b.id} as anything but ${avoid(b, beats[i + 1])}.`
24501
24512
  );
24502
24513
  }
24503
24514
  }
@@ -25814,13 +25825,39 @@ function operateVariants(v) {
25814
25825
  return [only(OPERATE_CHAIN, OPERATE_TERMS), only(OPERATE_TERMS)];
25815
25826
  }
25816
25827
  function schemaFor(prefs, source) {
25817
- return splitOperate(
25818
- hideFromPlanner(
25819
- forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
25820
- plannerInvisible(prefs, source)
25828
+ return shareLiteral(
25829
+ splitOperate(
25830
+ hideFromPlanner(
25831
+ forStructuredOutput(z3.toJSONSchema(storyboardSchema, { io: "input" })),
25832
+ plannerInvisible(prefs, source)
25833
+ )
25821
25834
  )
25822
25835
  );
25823
25836
  }
25837
+ function shareLiteral(schema2) {
25838
+ const root2 = schema2;
25839
+ const variants = root2.properties.beats.items.anyOf;
25840
+ const literal = variants[0]?.properties.literal;
25841
+ if (!literal) return schema2;
25842
+ const one2 = JSON.stringify(literal);
25843
+ if (!variants.every((v) => JSON.stringify(v.properties.literal) === one2))
25844
+ throw new Error("schemaFor: the beat variants carry different `literal` schemas.");
25845
+ const ref = { $ref: "#/$defs/literal" };
25846
+ return {
25847
+ ...root2,
25848
+ $defs: { literal },
25849
+ properties: {
25850
+ ...root2.properties,
25851
+ beats: {
25852
+ ...root2.properties.beats,
25853
+ items: {
25854
+ ...root2.properties.beats.items,
25855
+ anyOf: variants.map((v) => ({ ...v, properties: { ...v.properties, literal: ref } }))
25856
+ }
25857
+ }
25858
+ }
25859
+ };
25860
+ }
25824
25861
  var SCHEMA = schemaFor(prefsSchema.parse({}));
25825
25862
  async function codexPlanner(source, opts = {}) {
25826
25863
  const prefs = opts.prefs ?? prefsSchema.parse({});
@@ -25828,7 +25865,8 @@ async function codexPlanner(source, opts = {}) {
25828
25865
  try {
25829
25866
  const schemaPath = join11(dir, "storyboard.schema.json");
25830
25867
  const outPath = join11(dir, "storyboard.json");
25831
- await writeFile8(schemaPath, JSON.stringify(schemaFor(prefs, source)));
25868
+ const schema2 = schemaFor(prefs, source);
25869
+ await writeFile8(schemaPath, JSON.stringify(schema2));
25832
25870
  const ask = async (prompt2) => {
25833
25871
  await rm5(outPath, { force: true });
25834
25872
  await (opts.run ?? runCodex)({
@@ -25972,6 +26010,46 @@ ${left.map((m) => ` ${m}`).join("\n")}`
25972
26010
  );
25973
26011
  return plan;
25974
26012
  };
26013
+ const beatSchemaPath = join11(dir, "beat.schema.json");
26014
+ const offered = beatVariants(schema2).map((v) => v.properties.archetype.const);
26015
+ const reshape2 = async (deck, i) => {
26016
+ const beat = deck.beats[i];
26017
+ const shapes = shapesFor(deck, i, offered, prefs.images);
26018
+ if (!shapes.length) return deck;
26019
+ await writeFile8(beatSchemaPath, JSON.stringify(beatSchemaFor(schema2, shapes)));
26020
+ let problems = [];
26021
+ for (let t2 = 1; t2 <= SCENE_REPAIRS; t2++) {
26022
+ opts.onReshape?.(beat.id, shapes);
26023
+ const out = join11(dir, `beat-${beat.id}-${t2}.json`);
26024
+ try {
26025
+ await (opts.run ?? runCodex)({
26026
+ prompt: beatPrompt(prompt, deck, beat, shapes, problems),
26027
+ schemaPath: beatSchemaPath,
26028
+ outPath: out,
26029
+ ...opts.model === void 0 ? {} : { model: opts.model },
26030
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
26031
+ });
26032
+ const got = stripNulls(JSON.parse(await readFile9(out, "utf8")))?.beat;
26033
+ const redrawn = {
26034
+ ...got,
26035
+ id: beat.id,
26036
+ ...beat.literal ? { literal: beat.literal } : {}
26037
+ };
26038
+ const beats = deck.beats.map((b, k) => k === i ? redrawn : b);
26039
+ return withStockLabels(checkPlan({ ...deck, beats }, source, prefs), source);
26040
+ } catch (err) {
26041
+ problems = err instanceof PlanRejected ? err.problems : [`the ask failed: ${err instanceof Error ? err.message : err}`];
26042
+ }
26043
+ }
26044
+ return deck;
26045
+ };
26046
+ const neighbours = async (planned) => {
26047
+ let plan = planned;
26048
+ for (let i = 1; i < plan.beats.length; i++)
26049
+ if (plan.beats[i]?.archetype === plan.beats[i - 1]?.archetype)
26050
+ plan = await reshape2(plan, i);
26051
+ return plan;
26052
+ };
25975
26053
  let answer = await attempt2(prompt);
25976
26054
  if (answer.plan && listOf(answer.broken).length === 0) return await scenes(answer.plan);
25977
26055
  const firstBeats = beatCount(answer.json);
@@ -25991,7 +26069,13 @@ ${left.map((m) => ` ${m}`).join("\n")}`
25991
26069
  advice: []
25992
26070
  };
25993
26071
  if (next.plan && listOf(still).length === 0) return await scenes(next.plan);
25994
- if (round6 === REPAIRS) throw unrepaired(next, still, round6);
26072
+ if (round6 === REPAIRS) {
26073
+ if (!next.plan || !listOf(still).every(isNeighbour)) throw unrepaired(next, still, round6);
26074
+ const plan = await neighbours(next.plan);
26075
+ const left = { ...findings(plan), advice: [] };
26076
+ if (listOf(left).length) throw unrepaired(next, left, round6);
26077
+ return await scenes(plan);
26078
+ }
25995
26079
  merging ||= still.size.length > 0;
25996
26080
  answer = { ...next, broken: still };
25997
26081
  }
@@ -26046,6 +26130,37 @@ ${list2}
26046
26130
 
26047
26131
  Return {"literal": ...}: beat ${id2}'s scene, redrawn so none of these hold \u2014 every number, word and figure one the source states, every slot its verb needs filled. Keep its verb unless a finding names another. Return a scene: this ask has no way to leave it out. No other beat is yours to change.
26048
26132
 
26133
+ THE STORYBOARD:
26134
+ ${JSON.stringify(plan, null, 2)}`;
26135
+ }
26136
+ function isNeighbour(finding) {
26137
+ return /^\S+ and \S+ are both `[^`]+`, next to each other\./.test(finding);
26138
+ }
26139
+ function beatVariants(schema2) {
26140
+ return schema2.properties.beats.items.anyOf;
26141
+ }
26142
+ function beatSchemaFor(schema2, shapes) {
26143
+ const anyOf = beatVariants(schema2).filter((v) => shapes.includes(v.properties.archetype.const)).map((v) => hideFromPlanner(v, /* @__PURE__ */ new Set(["literal"])));
26144
+ return {
26145
+ type: "object",
26146
+ properties: { beat: { anyOf } },
26147
+ required: ["beat"],
26148
+ additionalProperties: false
26149
+ };
26150
+ }
26151
+ function beatPrompt(prompt, plan, beat, shapes, problems) {
26152
+ const refused = problems.length ? `
26153
+ Your last answer was refused for:
26154
+ ${problems.map((m) => ` - ${m}`).join("\n")}
26155
+ ` : "";
26156
+ return `${prompt}
26157
+
26158
+ THE DECK IS PLANNED. ONE BEAT'S SHAPE IS LEFT.
26159
+
26160
+ The storyboard below is settled, but beat ${beat.id} is \`${beat.archetype}\` beside a beat of the same shape, and no two beats in a row may share an archetype.
26161
+ ${refused}
26162
+ Return {"beat": ...}: beat ${beat.id} redrawn as one of ${shapes.map((s) => `\`${s}\``).join(", ")} \u2014 whichever shows its point best \u2014 with the params that shape needs, every number, word and figure one the source states. Keep its id, its point, its evidence, its narration and its part. No other beat is yours to change.
26163
+
26049
26164
  THE STORYBOARD:
26050
26165
  ${JSON.stringify(plan, null, 2)}`;
26051
26166
  }
@@ -26143,7 +26258,9 @@ async function readPlan(outPath, source, prefs) {
26143
26258
  ${raw2.slice(0, 200)}`
26144
26259
  );
26145
26260
  }
26146
- const plan = stripNulls(parsed);
26261
+ return checkPlan(stripNulls(parsed), source, prefs);
26262
+ }
26263
+ function checkPlan(plan, source, prefs) {
26147
26264
  const result = storyboardSchema.safeParse(plan);
26148
26265
  if (!result.success) {
26149
26266
  const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
@@ -31,6 +31,8 @@ export interface CodexOptions {
31
31
  onScene?: (beat: string, broken: readonly string[]) => void;
32
32
  /** Told the beats whose scene was dropped (still broken after its own repairs, or broken only by a symbolic quantity), and why. */
33
33
  onDropScenes?: (beats: readonly string[], why: readonly string[]) => void;
34
+ /** Told a beat still beside its own shape after the last repair is redrawn on its own, as one of `shapes`. */
35
+ onReshape?: (beat: string, shapes: readonly string[]) => void;
34
36
  /** Each source figure's panel count as the build will cut it, when the caller read them. */
35
37
  panels?: Readonly<Record<string, number>>;
36
38
  }
@@ -12,7 +12,8 @@
12
12
  * So the rule is a minimum, not an invitation, and it is CHECKED: the prompt
13
13
  * (src/plan/prompt.ts) states these numbers by importing them from here, and
14
14
  * `codexPlanner` (src/plan/codex.ts) runs `varietyFindings` on what comes back,
15
- * asks once for a repair, and refuses a plan that still breaks it. One table,
15
+ * asks for up to three repairs, redraws a beat still beside its own shape on
16
+ * its own (`shapesFor`), and refuses a plan that still breaks it. One table,
16
17
  * read by both, so the sentence the model is given and the check it is held to
17
18
  * cannot drift apart.
18
19
  *
@@ -90,6 +91,16 @@ export declare function scenesRequired(beats: number, images: Prefs["images"]):
90
91
  * the cards-on-paper the share exists to refuse.
91
92
  */
92
93
  export declare function isScene(beat: Beat, design?: Design): boolean;
94
+ /**
95
+ * The shapes, of those `offered`, that beat `i` may be redrawn as: not either
96
+ * neighbour's, not a title past the first beat, and not one that tips the
97
+ * kinetic or panel cap. What the last-resort redraw of one beat
98
+ * (src/plan/codex.ts) offers in its schema, so the shape cannot be wrong.
99
+ *
100
+ * ponytail: the A/B/A/B and scene-share rules are not foreseen here; the plan
101
+ * is checked whole after the redraw, and refused if one of them broke.
102
+ */
103
+ export declare function shapesFor(storyboard: Storyboard, i: number, offered: readonly Archetype[], images: Prefs["images"]): Archetype[];
93
104
  /**
94
105
  * Every way `storyboard` breaks the variety rule, as sentences the planner can
95
106
  * act on. Empty is a pass.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",