VerseCraft Writing Guide & Anti-Repetition Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Give DeepSeek thorough, composable VN-writing guidance (format/pacing, worldbuilding, prose, choices) and reduce the four kinds of repeated dialogue, via expanded craft blocks plus a deterministic near-duplicate node guard on AI chapter output.
Architecture: Add named prompt blocks to craft.ts and inject only the relevant ones into each generation step’s system prompt; add a pure dropDuplicateNodes pass called on assembled AI chapters. No data/schema/API/fallback changes.
Tech Stack: TypeScript, DeepSeek via OpenAI SDK, Vitest (node env).
Global Constraints¶
No DOM test env (
vitest.config.tsisenvironment: 'node'). Test pure helpers only; verify the rest via typecheck + lint.Run binaries directly:
./node_modules/.bin/vitest run <file>,./node_modules/.bin/eslint <files>, and for typechecknode --stack-size=4000 --max-old-space-size=8192 ./node_modules/typescript/bin/tsc --noEmit(baretscstack-overflows).Determinism / fallback untouched: do NOT modify
fallback.ts; the dedup guard applies to the AI path only.Scope: prompt content + composition + one pure dedup pass. No genre system, no settings-bank, no schema/API changes.
Tests live in
lib/versecraft/gen/__tests__/**/*.test.ts(glob already registered).Pre-existing baseline noise to ignore: 7 kowloon-knockout TS errors; lint errors in
components/versecraft/DialogueScreen.tsxandPoemPresentation.tsx.
File Structure¶
lib/versecraft/gen/craft.ts(modify) — addVN_FORMAT,PROSE_CRAFT,SETTING_CRAFT,CHOICE_CRAFT,ANTI_REPETITION; keepCRAFT_SYSTEM/craftDirectives.lib/versecraft/gen/dedupe.ts(create) — puredropDuplicateNodes.lib/versecraft/gen/generate.server.ts(modify) — compose blocks into chapter + world + outline prompts; calldropDuplicateNodesingenerateChapter.lib/versecraft/gen/__tests__/craft.test.ts(modify) — assert each new block’s key rules.lib/versecraft/gen/__tests__/dedupe.test.ts(create) — unit tests for the guard.
Task 1: Writing-guide blocks in craft.ts¶
Files:
Modify:
lib/versecraft/gen/craft.tsTest:
lib/versecraft/gen/__tests__/craft.test.ts
Interfaces:
Consumes: nothing.
Produces (all
const … : stringexports):VN_FORMAT,PROSE_CRAFT,SETTING_CRAFT,CHOICE_CRAFT,ANTI_REPETITION(alongside existingCRAFT_SYSTEM,craftDirectives).Step 1: Add the failing tests
Append inside lib/versecraft/gen/__tests__/craft.test.ts. First update its import line to:
import {
CRAFT_SYSTEM, craftDirectives,
VN_FORMAT, PROSE_CRAFT, SETTING_CRAFT, CHOICE_CRAFT, ANTI_REPETITION,
} from '../craft';
Then add these tests (inside the existing describe('craft module', …) block, or a new describe):
describe('writing-guide blocks', () => {
it('VN_FORMAT covers pacing, hooks, and registers', () => {
const s = VN_FORMAT.toLowerCase();
expect(s).toContain('scene');
expect(s).toContain('hook');
expect(s).toContain('narration');
});
it('PROSE_CRAFT bans clichés and demands distinct voices', () => {
const s = PROSE_CRAFT.toLowerCase();
expect(s).toContain('clich');
expect(s).toContain('distinct');
});
it('SETTING_CRAFT covers worldbuilding', () => {
const s = SETTING_CRAFT.toLowerCase();
expect(s).toContain('world');
expect(s).toContain('place');
});
it('CHOICE_CRAFT requires materially distinct options', () => {
const s = CHOICE_CRAFT.toLowerCase();
expect(s).toContain('direction');
expect(s).toContain('different');
});
it('ANTI_REPETITION forbids repeats within and across chapters', () => {
const s = ANTI_REPETITION.toLowerCase();
expect(s).toContain('repetition');
expect(s).toContain('story so far');
});
});
Step 2: Run the tests to verify they fail
Run: ./node_modules/.bin/vitest run lib/versecraft/gen/__tests__/craft.test.ts
Expected: FAIL — VN_FORMAT/etc. are not exported.
Step 3: Add the blocks to craft.ts
In lib/versecraft/gen/craft.ts, after the existing CRAFT_SYSTEM declaration (before craftDirectives), add:
export const VN_FORMAT =
'VISUAL-NOVEL FORMAT & PACING:\n' +
'- Keep scenes tight and moving — every scene earns its place; cut anything that wanders.\n' +
'- Balance the registers: narration (speaker=null), the player\'s inner thoughts (speaker=null), and spoken lines (cast ids, or "mc" for the player aloud). Never stack many narration nodes in a row.\n' +
'- End every scene — and the chapter — on a hook, a turn, or an unresolved beat that pulls the reader onward.\n' +
'- Let emotion shift line to line and LAND on beats; do not hold one flat emotion across a scene.\n' +
'- Vary which characters are present and who speaks; avoid long two-character talking-heads stretches.\n' +
'- Reveal through scene, not summary — no info-dumps; trust the reader.';
export const PROSE_CRAFT =
'PROSE:\n' +
'- Prefer concrete, sensory verbs and nouns over adjectives and abstraction.\n' +
'- Ban stock/clichéd phrasing (e.g. "a single tear", "little did they know", "time seemed to stop", "a breath they didn\'t know they were holding").\n' +
'- Give each character a DISTINCT diction, rhythm, and vocabulary per their bible voice — two characters must never phrase things the same way.\n' +
'- Restraint over melodrama; understate the biggest moments. No purple prose.\n' +
'- Vary imagery and sentence shapes — do not keep reaching for the same words, images, or constructions.';
export const SETTING_CRAFT =
'SETTING & WORLDBUILDING:\n' +
'- Build a coherent, lived-in sense of place: specific textures, routines, sounds, and small details that imply a world beyond the frame.\n' +
'- Give the world an internal logic and honor it consistently.\n' +
'- Ground emotion in sensory specificity tied to THIS place, not a generic backdrop.\n' +
'- Choose motifs that belong to this world and let them recur meaningfully.\n' +
'- Keep tone and genre consistent, flexing to the player\'s prompt without breaking the emotional, character-driven romance/drama frame.';
export const CHOICE_CRAFT =
'CHOICES:\n' +
'- The 2–3 options must be genuinely different MOVES — distinct content AND consequence, never reworded versions of one another.\n' +
'- Each option\'s "direction" must be materially distinct (a different thing the player actually does).\n' +
'- Include occasional bad or costly options that damage a bond — not every choice is safe.';
export const ANTI_REPETITION =
'NO REPETITION:\n' +
'- Never restate a line, image, or sentiment already used earlier in THIS chapter; every node must advance, not echo.\n' +
'- Never reuse a line, exchange, image, or beat from the STORY SO FAR (the ledger and earlier chapters).\n' +
'- Once you have made a point, move on — do not have characters circle back to re-say it.';
Step 4: Run the tests to verify they pass
Run: ./node_modules/.bin/vitest run lib/versecraft/gen/__tests__/craft.test.ts
Expected: PASS (all craft tests, new and old).
Step 5: Commit
git add lib/versecraft/gen/craft.ts lib/versecraft/gen/__tests__/craft.test.ts
git commit -m "feat(versecraft): composable VN writing-guide blocks (format, prose, setting, choice, anti-repetition)"
Task 2: Deterministic near-duplicate node guard¶
Files:
Create:
lib/versecraft/gen/dedupe.tsCreate:
lib/versecraft/gen/__tests__/dedupe.test.ts
Interfaces:
Consumes:
GenScenefrom./world-types.Produces:
function dropDuplicateNodes(scenes: GenScene[]): GenScene[]— returns new scenes with non-choice nodes whose normalized text exactly repeats an earlier node (anywhere in the chapter) removed; choice nodes always kept; every scene keeps ≥1 node.Step 1: Write the failing tests
Create lib/versecraft/gen/__tests__/dedupe.test.ts:
import { describe, it, expect } from 'vitest';
import { dropDuplicateNodes } from '../dedupe';
import type { GenScene } from '../world-types';
function scene(id: string, nodes: { text: string; choices?: unknown[] }[]): GenScene {
return {
id, environment: 'cafe', timeOfDay: 'morning', charactersPresent: ['c1'],
nodes: nodes.map((n, i) => ({
id: `${id}_n${i}`, speaker: null, text: n.text,
...(n.choices ? { choices: n.choices } : {}),
})),
} as GenScene;
}
describe('dropDuplicateNodes', () => {
it('drops a later node that exactly repeats an earlier one', () => {
const out = dropDuplicateNodes([scene('s0', [
{ text: 'The rain falls.' }, { text: 'She smiles.' }, { text: 'the rain falls' },
])]);
expect(out[0].nodes.map(n => n.text)).toEqual(['The rain falls.', 'She smiles.']);
});
it('treats punctuation/case/whitespace differences as duplicates', () => {
const out = dropDuplicateNodes([scene('s0', [
{ text: 'You came back.' }, { text: 'you came back!!!' },
])]);
expect(out[0].nodes).toHaveLength(1);
});
it('dedupes across scenes', () => {
const out = dropDuplicateNodes([
scene('s0', [{ text: 'A line.' }]),
scene('s1', [{ text: 'a line' }, { text: 'New.' }]),
]);
expect(out[1].nodes.map(n => n.text)).toEqual(['New.']);
});
it('never drops a choice node even if its text repeats', () => {
const out = dropDuplicateNodes([scene('s0', [
{ text: 'Pick one.' }, { text: 'pick one', choices: [{ text: 'a' }] },
])]);
expect(out[0].nodes).toHaveLength(2);
});
it('keeps the first node when a scene would otherwise go empty', () => {
const out = dropDuplicateNodes([
scene('s0', [{ text: 'Hi.' }]),
scene('s1', [{ text: 'hi' }]),
]);
expect(out[1].nodes).toHaveLength(1);
expect(out[1].nodes[0].text).toBe('hi');
});
});
Step 2: Run the tests to verify they fail
Run: ./node_modules/.bin/vitest run lib/versecraft/gen/__tests__/dedupe.test.ts
Expected: FAIL — cannot find module ../dedupe.
Step 3: Write the implementation
Create lib/versecraft/gen/dedupe.ts:
// ─── Near-duplicate node guard ────────────────────────────────────────────────
// Strips repeated nodes from AI chapter output. The deterministic fallback writer
// already de-dups via pickUnique; AI output has no such guard, so the same line
// can recur within or across a chapter's scenes. Pure + deterministic.
import type { GenScene } from './world-types';
/** Normalize text for duplicate detection: lowercase, drop non-alphanumerics,
* collapse whitespace. */
function normalize(text: string): string {
return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim();
}
/** Remove non-choice nodes whose normalized text exactly repeats one already seen
* earlier in the chapter. Choice nodes are always kept (they drive progression),
* and every scene retains at least its first node so none goes empty. */
export function dropDuplicateNodes(scenes: GenScene[]): GenScene[] {
const seen = new Set<string>();
return scenes.map((scene) => {
const kept = scene.nodes.filter((node) => {
if (node.choices?.length) return true; // never drop a choice node
const key = normalize(node.text);
if (!key) return true; // keep empties / punctuation-only ("...")
if (seen.has(key)) return false;
seen.add(key);
return true;
});
return { ...scene, nodes: kept.length ? kept : [scene.nodes[0]] };
});
}
Step 4: Run the tests to verify they pass
Run: ./node_modules/.bin/vitest run lib/versecraft/gen/__tests__/dedupe.test.ts
Expected: PASS (5 tests).
Step 5: Commit
git add lib/versecraft/gen/dedupe.ts lib/versecraft/gen/__tests__/dedupe.test.ts
git commit -m "feat(versecraft): deterministic near-duplicate node guard for AI chapters"
Task 3: Compose blocks into prompts + apply the dedup guard¶
Files:
Modify:
lib/versecraft/gen/generate.server.ts
Interfaces:
Consumes:
VN_FORMAT,PROSE_CRAFT,SETTING_CRAFT,CHOICE_CRAFT,ANTI_REPETITION(./craft);dropDuplicateNodes(./dedupe).Produces: no new exports; behavior change to
chapterSystemPrompt(),generateWorld,generateOutline,generateChapter.Step 1: Extend the craft import and add the dedupe import
In lib/versecraft/gen/generate.server.ts, replace:
import { CRAFT_SYSTEM, craftDirectives } from './craft';
with:
import {
CRAFT_SYSTEM, craftDirectives,
VN_FORMAT, PROSE_CRAFT, SETTING_CRAFT, CHOICE_CRAFT, ANTI_REPETITION,
} from './craft';
import { dropDuplicateNodes } from './dedupe';
Step 2: Compose the new blocks into
chapterSystemPrompt()
In chapterSystemPrompt(), replace this line:
CRAFT_SYSTEM + '\n' +
'Respond ONLY with a JSON object.'
with:
VN_FORMAT + '\n' +
CRAFT_SYSTEM + '\n' +
PROSE_CRAFT + '\n' +
CHOICE_CRAFT + '\n' +
ANTI_REPETITION + '\n' +
'Respond ONLY with a JSON object.'
Step 3: Add setting + prose guidance to the
generateWorldsystem prompt
In generateWorld, replace this line (end of the const system = string):
'and care, never gratuitously or as shock value. Respond ONLY with a JSON object.';
with:
'and care, never gratuitously or as shock value. ' +
SETTING_CRAFT + '\n' + PROSE_CRAFT + '\n' +
'Respond ONLY with a JSON object.';
Step 4: Add one pacing line to the
generateOutlinesystem prompt
In generateOutline, replace:
'You are the showrunner for an emotional anime visual novel. You design a tight dramatic arc with setups ' +
'planted early and paid off later, one beat per chapter. Respond ONLY with a JSON object.';
with:
'You are the showrunner for an emotional anime visual novel. You design a tight dramatic arc with setups ' +
'planted early and paid off later, one beat per chapter. Build rising tension across the five acts, and make ' +
'each chapter a DISTINCT beat that does not echo another. Respond ONLY with a JSON object.';
Step 5: Apply the dedup guard in
generateChapter
In generateChapter, replace:
const scenes = continuing ? [opening!, ...rest] : rest;
if (!scenes.length) return fallbackChapter(world, index);
return {
index, act: routeBeat.act,
title: parsed.title || routeBeat.title,
subtitle: parsed.subtitle || `Act ${routeBeat.act} — ${world.title}`,
emotionalGoal: routeBeat.emotionalGoal,
scenes,
source: 'ai',
};
with:
const scenes = continuing ? [opening!, ...rest] : rest;
const deduped = dropDuplicateNodes(scenes);
if (!deduped.length) return fallbackChapter(world, index);
return {
index, act: routeBeat.act,
title: parsed.title || routeBeat.title,
subtitle: parsed.subtitle || `Act ${routeBeat.act} — ${world.title}`,
emotionalGoal: routeBeat.emotionalGoal,
scenes: deduped,
source: 'ai',
};
Step 6: Typecheck
Run: node --stack-size=4000 --max-old-space-size=8192 ./node_modules/typescript/bin/tsc --noEmit 2>&1 | grep -i versecraft || echo "0 versecraft type errors"
Expected: 0 versecraft type errors.
Step 7: Lint
Run: ./node_modules/.bin/eslint lib/versecraft/gen/generate.server.ts lib/versecraft/gen/craft.ts lib/versecraft/gen/dedupe.ts
Expected: no errors.
Step 8: Run the full gen suite (regression)
Run: ./node_modules/.bin/vitest run lib/versecraft/gen/__tests__
Expected: all suites PASS (craft, dedupe, choice-path, ledger, outline, bible, generate-fallback).
Step 9: Commit
git add lib/versecraft/gen/generate.server.ts
git commit -m "feat(versecraft): inject writing-guide blocks into prompts; dedup AI chapter nodes"
Final verification¶
Step 1: Full gen suite —
./node_modules/.bin/vitest run lib/versecraft/gen/__tests__→ all pass.Step 2: Typecheck —
node --stack-size=4000 --max-old-space-size=8192 ./node_modules/typescript/bin/tsc --noEmit→ no NEW errors (only the 7 kowloon baseline).Step 3: Lint —
./node_modules/.bin/eslint lib/versecraft→ no NEW errors (DialogueScreen/PoemPresentation baseline excepted).Step 4: Senior review — run the
senior-swe-revieweragent on the branch diff before opening the PR (repo convention for game work).
Self-Review¶
Spec coverage: VN_FORMAT/PROSE_CRAFT/SETTING_CRAFT/CHOICE_CRAFT/ANTI_REPETITION blocks → Task 1; composition into chapter/world/outline prompts → Task 3 (steps 2–4); dedup guard module → Task 2; dedup applied in generateChapter (incl. continuation seeding via opening scene being first in
scenes) → Task 3 step 5; scribe unchanged (correct — not in any task). All spec sections covered.Placeholders: none — every code step shows complete code.
Type consistency:
dropDuplicateNodes(scenes: GenScene[]): GenScene[]defined in Task 2 and imported/called identically in Task 3; the five craft block names match between Task 1 (definition), Task 1 test, and Task 3 import.