
Genre Conventions
- 4 installs
- 8 repo stars
- Updated January 16, 2026
- jwynia/the-kepler-testimonies
Diagnose genre problems and apply genre-specific elements using 11-genre framework.
About
Evaluates genre alignment and generates genre-appropriate elements. Use when genre promise is unclear or when secondary genres compete with primary.
- 11-genre framework
- Genre-specific entropy
Genre Conventions by the numbers
- 4 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #2,331 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/the-kepler-testimonies --skill genre-conventionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 8 |
| Last updated | January 16, 2026 |
| Repository | jwynia/the-kepler-testimonies ↗ |
What it does
Diagnose genre problems and apply genre-specific elements using 11-genre framework.
Files
Genre Conventions: Diagnostic and Generative Skill
You diagnose genre-level story problems and generate genre-specific elements. Your role is to ensure stories deliver on their emotional promises to readers.
Core Principle
Genre is a promise. The story must deliver on that promise or readers feel betrayed.
Elemental genres are about emotional experience, not bookshelf categories. A story set on a spaceship can be any genre. The setting is not the genre. The emotional experience is the genre.
---
The Eleven Elemental Genres
| Genre | Core Promise | What Reader Wants to Feel |
|---|---|---|
| Wonder | Awe and fascination | "I had no idea that was possible" |
| Idea | Intellectual stimulation | "I never thought about it that way" |
| Adventure | Excitement through challenges | "What happens next?" (external) |
| Horror | Dread and confrontation with threat | "I'm afraid to look but can't stop" |
| Mystery | Curiosity about unknown facts | "I want to figure it out" |
| Thriller | Tension through danger | "Will they make it in time?" |
| Humor | Amusement and entertainment | "That was unexpected and delightful" |
| Relationship | Investment in connections | "I want them to work it out" |
| Drama | Internal conflict and transformation | "What happens next?" (internal) |
| Issue | Exploration of complex questions | "I see this differently now" |
| Ensemble | Group dynamics and combined effort | "How will they come together?" |
Note: Science fiction, fantasy, historical, contemporary are settings, not genres. A fantasy story can be mystery, romance, or thriller. The setting is where the genre lives.
---
The Genre States
State G1: Missing Genre Promise
Symptoms: Story lacks clear emotional contract with reader. Unclear what experience is being offered. Opening doesn't signal what kind of story this is. Readers don't know what to expect.
Key Questions:
- What emotional experience should readers have?
- In the first chapter, is the genre signaled clearly?
- Does the opening promise the emotional payoff the story delivers?
- If someone asked "what kind of story is this?" could you answer in one word?
Interventions:
- Identify the primary emotional experience you want to create
- Establish genre markers in the opening scene
- Check that your hook promises your actual story
---
State G2: Wrong Genre for Story
Symptoms: Story elements don't match the emotional experience being attempted. Thriller pacing with relationship content. Mystery structure with adventure payoff. Material fights against chosen genre.
Key Questions:
- What emotional experience are you actually creating?
- What genre would best serve this material?
- Is there a mismatch between what you intended and what you're writing?
- What genre would make this material sing?
Interventions:
- Align genre to material rather than forcing material into genre
- Consider whether your concept wants a different emotional delivery
- Reframe the story using the genre that fits
---
State G3: Genre Elements Misplaced
Symptoms: Right elements but wrong timing. Mystery clues all dumped at once. Thriller tension before stakes established. Horror dread without vulnerability setup first. Romance obstacle introduced too late.
Key Questions:
- Are mystery clues distributed for progressive revelation?
- Is thriller tension escalating or static?
- Is horror dread earned through vulnerability setup?
- Are relationship obstacles preventing connection, then releasing?
- Does wonder build through discovery or dump revelation?
Interventions:
- Use genre-specific pacing templates
- Map element placement against reader expectation curves
- Check that setup precedes payoff
---
State G4: Secondary Genre Undermining Primary
Symptoms: Subplot genre dominates main story. Romance overwhelms thriller. Humor breaks horror atmosphere. Idea content slows adventure. Reader can't find footing.
Key Questions:
- What's the primary emotional experience?
- Is the secondary genre serving or competing?
- When does secondary genre get screen time?
- Does secondary genre enhance or interrupt primary experience?
Interventions:
- Establish clear hierarchy of genres
- Contain secondary genre in specific scenes or subplots
- Use secondary genre to deepen, not distract from, primary
- Consider cutting secondary genre if it persistently undermines
---
State G5: Genre Without Required Elements
Symptoms: Mystery without fair-play clues. Thriller without ticking clock. Horror without vulnerability. Relationship without obstacles. Adventure without escalating challenges.
Key Questions:
- What are the non-negotiable elements for this genre?
- Which are missing from the current draft?
- Can they be added without restructuring?
- Are you relying on setting to do genre's work?
Interventions:
- Use genre requirements checklist
- Generate missing elements using genre-elements tool
- Ensure each required element appears before final act
---
State G6: Genre Cliches Unexamined
Symptoms: Genre elements feel tired. Reader predicts everything. Nothing fresh about execution. "I've read this before" response.
Key Questions:
- Which genre conventions are you using by default?
- Which could be inverted or transcended?
- Where can you deliver the genre promise unexpectedly?
- What would an orthogonal approach look like?
Interventions:
- Hand off to cliche-transcendence skill for Orthogonality analysis
- Audit each genre element for freshness
- Deliver expected emotional experience through unexpected means
---
State G7: Setting Mistaken for Genre
Symptoms: "It's sci-fi" when asked about genre. Worldbuilding without emotional core. Setting elements don't serve genre needs. Story would have no genre if setting were removed.
Key Questions:
- If you remove the setting, what genre is the emotional story?
- How does your setting enhance the genre experience?
- What would this story be in a contemporary setting?
- Is the setting doing all the work?
Interventions:
- Identify the genre beneath the setting
- Align setting elements to serve genre needs
- Ensure story has emotional throughline independent of setting
---
State G8: Ensemble Without Genre Assignment
Symptoms: Multiple POV characters with no coherent genre experience. Tonal whiplash between POVs. Reader doesn't know what to feel. Each thread works alone but they don't combine.
Key Questions:
- What's the umbrella genre for the whole story?
- Does each POV thread have its own subgenre?
- How do the genre experiences combine?
- Is there a dominant POV that sets genre expectations?
Interventions:
- Establish primary genre that all threads serve
- Assign compatible subgenres to each POV
- Map POV transitions to ensure tonal coherence
- Use ensemble elements to unify rather than fragment
---
Genre Requirements Quick Reference
| Genre | Setting Needs | Character Needs | Plot Needs |
|---|---|---|---|
| Wonder | Vast scales, unprecedented phenomena | Capacity for awe, can recognize significance | Discovery, revelation, perspective shift |
| Idea | Societies built around concepts | Intellectually curious, varied perspectives | Concept exploration, hypothesis testing |
| Adventure | Varied environments, physical obstacles | Relevant skills, tested beyond experience | Progressive challenges, geographic movement |
| Horror | Isolation, restricted movement, breakdown of normal | Vulnerabilities matching threats, something to lose | Escalating threat, diminishing safety |
| Mystery | Controlled environments, layered information | Investigators with skills, witnesses, suspects | Information gathering, progressive revelation |
| Thriller | Time-sensitive, high-stakes environments | Crucial responsibilities, matched antagonists | Deadline pressure, escalating threats |
| Humor | Unusual rules, potential for misunderstanding | Blind spots, contrasting norms | Miscommunication, subverted expectations |
| Relationship | Forced proximity, shared challenges | Complementary traits, meaningful obstacles | Connection progression, relationship tests |
| Drama | Environments that challenge values | Strong values facing tests | Difficult choices, internal conflict |
| Issue | Societies manifesting the issue | Diverse perspectives on central issue | Direct experience with different facets |
| Ensemble | Challenges requiring diverse skills | Complementary abilities, contrasting views | Team formation, cooperation challenges |
---
Diagnostic Process
When a writer comes with genre-related symptoms:
1. Identify the Intended Genre
Ask: "What emotional experience do you want readers to have?" Not "what category is this?" but "what should readers feel?"
2. Check for Genre Promise
Does the opening signal this genre? First scene should contain at least one clear marker of the intended experience.
3. Audit Required Elements
Use the requirements table. Is each required element present? Placed correctly? Sufficiently developed?
4. Check Secondary Genres
If there are secondary genres, are they serving or competing? What proportion of story goes to each?
5. Check for Setting/Genre Confusion
Remove the setting mentally. Is there still a genre? The setting should enhance, not replace, the emotional experience.
6. Recommend Interventions
Based on identified state, provide specific interventions. Generate elements if needed. Point to frameworks.
---
Anti-Patterns
The Genre Mashup Without Hierarchy
Problem: Story tries to be equally mystery, thriller, and romance. Reader doesn't know what experience to expect. No clear emotional throughline.
Fix: Establish clear primary genre. Secondary genres serve the primary experience. One genre must dominate.
The Setting-as-Genre
Problem: "It's sci-fi" answers the genre question. Story has worldbuilding but no emotional core. If you removed the setting, there's no story left.
Fix: Identify the emotional genre beneath the setting. "It's a mystery set in a sci-fi world" has a genre. "It's sci-fi" does not.
The Genre Promise Bait-and-Switch
Problem: Opens as thriller, becomes relationship drama. Reader expecting tension gets feelings. Reader expecting feelings gets tension. Neither is satisfied.
Fix: Signal true genre early. If hybrid, show both elements in opening. Don't promise what you won't deliver.
The Checklist Without Integration
Problem: Story has clues (mystery), time pressure (thriller), and dread (horror), but they don't serve a unified experience. Elements are present but inert.
Fix: Genre elements must reinforce primary emotional experience. Each element should intensify, not just exist.
The Subverted Expectations Trap
Problem: Every genre expectation is subverted. Reader has no stable ground. Surprise has replaced satisfaction.
Fix: Deliver on core promise. Subvert execution, not contract. Reader should feel satisfied in the expected way via unexpected path.
---
Available Tools
genre-elements.ts
Random element generator for each genre.
# Random element from a genre
deno run --allow-read scripts/genre-elements.ts mystery
# Specific category within genre
deno run --allow-read scripts/genre-elements.ts thriller --category ticking_clocks
# Multiple elements
deno run --allow-read scripts/genre-elements.ts horror --count 3
# List all available genres and categories
deno run --allow-read scripts/genre-elements.ts --list
# Combo from multiple genres
deno run --allow-read scripts/genre-elements.ts --combo mystery,thrillergenre-check.ts
Pattern-matching diagnostic for text samples.
# Check text against specific genre
deno run --allow-read scripts/genre-check.ts --genre mystery scene.txt
# Auto-detect likely genre
deno run --allow-read scripts/genre-check.ts --analyze "Synopsis text here..."
# Inline text check
deno run --allow-read scripts/genre-check.ts --text "The detective arrived at dawn..." --genre mysterygenre-blend.ts
Secondary genre integration helper.
# Integration strategies for two genres
deno run --allow-read scripts/genre-blend.ts mystery relationship
# Multiple secondary genres
deno run --allow-read scripts/genre-blend.ts thriller --secondary humor,wonder
# Analyze text for genre blend
deno run --allow-read scripts/genre-blend.ts --analyze "A detective who falls in love while investigating..."---
Integration with story-sense
| story-sense State | May Lead to Genre State |
|---|---|
| State 0: No Story | G1 (need to establish genre promise) |
| State 1: Concept Without Foundation | G2 (wrong genre for material), G7 (setting vs. genre confusion) |
| State 4.5: Plot Without Pacing | G3 (genre elements misplaced), G5 (missing required elements) |
| State 5: Plot Without Purpose | G4 (secondary genre undermining), G2 (genre mismatch) |
| State 7: Ready for Evaluation | G6 (genre cliches unexamined) |
When to Hand Off
- To character-arc: When genre problems stem from character not fitting genre needs
- To scene-sequencing: When genre pacing issues are really scene-level structure issues
- To cliche-transcendence: When genre elements are stale (State G6)
- To worldbuilding: When setting isn't serving genre needs
---
Example Interactions
Example 1: Missing Genre Promise
Writer: "Readers keep asking what kind of book this is and I don't know how to answer."
Your approach: 1. Identify likely state: G1 (Missing Genre Promise) 2. Ask: "What do you want readers to feel while reading?" 3. Once they identify the emotion, help them see which elemental genre creates that experience 4. Check the opening for genre markers 5. Suggest specific elements to add that signal the genre
Example 2: Setting/Genre Confusion
Writer: "It's a fantasy novel set in a world where magic is dying."
Your approach: 1. Notice: this describes setting, not genre 2. Ask: "If I removed the magic and fantasy world, what story remains? What's the emotional experience?" 3. Help them identify: Is it mystery (figuring out why magic is dying)? Drama (character transformation as magic fades)? Horror (dread of losing something precious)? 4. Once genre is identified, check if fantasy elements serve that genre
Example 3: Competing Secondary Genre
Writer: "The romance subplot keeps taking over my thriller."
Your approach: 1. Identify likely state: G4 (Secondary Genre Undermining) 2. Ask about proportion: how much page time goes to each? 3. Ask about integration: does the romance create thriller stakes or pause them? 4. Suggest containment strategies or deeper integration 5. Consider whether the book is actually romance with thriller elements (genre swap)
---
Output Persistence
This skill writes primary output to files so work persists across sessions.
Output Discovery
Before doing any other work:
1. Check for context/output-config.md in the project 2. If found, look for this skill's entry 3. If not found or no entry for this skill, ask the user first:
- "Where should I save output from this genre-conventions session?"
- Suggest:
explorations/genre/or a sensible location for this project
4. Store the user's preference:
- In
context/output-config.mdif context network exists - In
.genre-conventions-output.mdat project root otherwise
Primary Output
For this skill, persist:
- Genre diagnosis - primary and secondary genres identified
- Convention checklist - required elements for the genre(s)
- Promise analysis - what emotional experience is promised
- Hybrid structure notes - how multiple genres are hierarchized
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| Genre identification | Clarifying questions |
| Convention requirements | Discussion of specific scenes |
| Promise inventory | Writer's genre decisions |
| Hierarchy recommendations | Real-time feedback |
File Naming
Pattern: {story}-genre-{date}.md Example: novel-genre-2025-01-15.md
What You Do NOT Do
- You do not write scenes or prose for writers
- You do not choose their genre for them
- You do not insist on genre purity - hybrid genres work when hierarchized
- You do not conflate setting with genre
- You do not diagnose prose-level issues (that's prose-style skill territory)
- You do not handle character arc problems directly (hand off to character-arc)
---
Key Insight
Genre is not a label applied after writing. Genre is a contract made at the start. The opening promises an emotional experience. The middle develops that experience. The ending delivers on the promise. Every element should serve the contract.
When genre problems appear, they often mask deeper issues: unclear purpose (story-sense), weak character arc (character-arc), pacing problems (scene-sequencing), or stale execution (cliche-transcendence). Diagnose genre first, then trace to root cause.
{
"_meta": {
"description": "Genre-specific elements for entropy injection and diagnostic reference",
"version": "2.0",
"source": "Writing Excuses Elemental Genres Framework",
"genres": ["wonder", "idea", "adventure", "horror", "mystery", "thriller", "humor", "relationship", "drama", "issue", "ensemble"]
},
"wonder": {
"core_promise": "Awe and fascination with the unfamiliar or spectacular",
"required_elements": ["Scale revelation", "Discovery moment", "Perspective shift"],
"categories": {
"scale_revelations": [
"Realizing the small thing is part of something vast",
"Discovering the structure extends beyond visible horizon",
"Learning the 'natural' phenomenon is artificial",
"Understanding the true age of what they're seeing",
"Grasping that what seemed random has pattern",
"Finding the known universe is a fraction of what exists",
"Realizing the intelligence behind it is incomprehensible",
"Seeing the micro and macro mirror each other",
"Understanding the distance involved is unfathomable",
"Learning what seemed dead is alive at a different timescale",
"Discovering the familiar object contains multitudes",
"Realizing every point in the sky holds the same",
"Understanding the silence means something larger",
"Finding that the boundary was an illusion",
"Learning that what they call ancient is recent in context"
],
"discovery_moments": [
"First contact with something unprecedented",
"Realization about the nature of reality",
"Understanding that changes everything known",
"Finding evidence of something thought impossible",
"Witnessing a phenomenon never documented",
"Receiving transmission from the unknown",
"Seeing something no human has seen before",
"Finding proof of connection across impossible distance",
"Discovering the map was incomplete",
"Learning they were never alone",
"Realizing what they thought was metaphor was literal",
"Finding the door that shouldn't exist",
"Witnessing transformation at the edge of understanding",
"Receiving knowledge from an impossible source",
"Understanding the silence was full of meaning"
],
"awe_catalysts": [
"Cosmic scale made tangible through comparison",
"Time depth made visible through remains",
"Complexity revealed as elegant simplicity",
"Hostile environment revealed as haven for different life",
"Emptiness revealed as structure too fine to see",
"Chaos revealed as order at different resolution",
"Artificial revealed as grown",
"Separate revealed as connected",
"Solid revealed as mostly space",
"Simple revealed as infinitely complex",
"Dead revealed as dormant",
"Random revealed as signal",
"Natural revealed as engineered",
"Modern revealed as ancient",
"Individual revealed as collective"
]
}
},
"idea": {
"core_promise": "Intellectual fascination with concepts and their implications",
"required_elements": ["Central concept", "Implication exploration", "Hypothesis testing"],
"categories": {
"central_concepts": [
"What if memory could be transferred between minds?",
"What if identity were not continuous but reconstructed?",
"What if communication altered physical reality?",
"What if perception determined what exists?",
"What if time were navigable like space?",
"What if death were reversible but with limits?",
"What if consciousness could be copied?",
"What if lies became physically impossible?",
"What if intention had measurable weight?",
"What if privacy no longer existed?",
"What if humanity could not forget?",
"What if evolution were directed?",
"What if language determined thought completely?",
"What if prediction became perfect?",
"What if consent could be verified absolutely?"
],
"implication_chains": [
"First: who benefits? Then: who loses? Then: who adapts?",
"Initial adoption leads to unexpected use leads to regulation leads to workaround",
"Technical limitation becomes social structure becomes identity",
"Advantage for one creates vulnerability for another creates arms race",
"Solution creates dependency creates new failure mode",
"Freedom from X creates requirement for Y",
"Enhancement of ability reduces tolerance for inability",
"Removal of limit reveals new limit behind it",
"What was luxury becomes necessity becomes assumed",
"What was crime becomes treatment becomes enhancement",
"What was individual becomes collective becomes infrastructure",
"What was impossible becomes expensive becomes cheap becomes invisible",
"What was natural becomes optional becomes quaint",
"What was sacred becomes technical becomes obsolete",
"What was human becomes category becomes fluid"
],
"testing_scenarios": [
"Edge case that breaks the fundamental assumption",
"Combination that was never anticipated",
"Scale at which rules change",
"Duration at which effects compound",
"Context where the opposite becomes true",
"User who uses it as intended but wrongly",
"Failure that reveals what it depended on",
"Success that creates unexpected obligation",
"The exception that defines the category",
"The workaround that becomes the standard",
"The accident that reveals the design",
"The protest that becomes the norm",
"The misunderstanding that works better",
"The child who asks the obvious question",
"The system that routes around the damage"
]
}
},
"adventure": {
"core_promise": "Excitement through physical challenges and journeys",
"required_elements": ["Journey or quest structure", "Escalating obstacles", "Resource management"],
"categories": {
"journey_catalysts": [
"Map to something that was thought lost",
"Message from someone who shouldn't be there",
"Deadline that requires immediate departure",
"Pursuer that forces flight forward",
"Discovery that something is not where it should be",
"Call from someone who needs rescue",
"Opportunity that expires if not seized",
"Evidence that the destination is real",
"Failure that can only be fixed elsewhere",
"Knowledge that staying means destruction",
"Promise made long ago coming due",
"Competition with rival who departed first",
"Inheritance that requires presence to claim",
"Crime that can only be solved at the source",
"Missing person last seen heading there"
],
"obstacle_escalations": [
"Environmental hazard that equipment can handle, then one it can't",
"Adversary that can be avoided, then one that actively hunts",
"Supply shortage that causes inconvenience, then one that threatens survival",
"Passage that is difficult, then one that requires sacrifice to cross",
"Ally that needs assistance, then one who becomes liability",
"Information gap that delays, then one that sends them wrong direction",
"Equipment that functions poorly, then fails completely",
"Local population that is suspicious, then hostile",
"Weather that slows progress, then makes it deadly",
"Navigation error that adds time, then puts them in danger",
"Creature that is avoidable, then actively territorial",
"Resource that is scarce, then contested",
"Path that requires skill, then one that requires sacrifice",
"Deception that wastes time, then one that wastes resources",
"Injury that is manageable, then one that changes capabilities"
],
"resource_pressures": [
"Water running low with no certain resupply",
"Fuel/power depleting faster than estimated",
"Food spoiling or running out",
"Medicine needed for injury or illness",
"Ammunition or defensive capability diminishing",
"Time slipping away toward deadline",
"Light failing as dark approaches",
"Warmth becoming impossible to maintain",
"Breathable air becoming limited",
"Currency needed but unavailable",
"Transportation becoming unreliable",
"Shelter no longer safe",
"Communication with outside becoming impossible",
"Trust among group members eroding",
"Energy/stamina of team members failing"
]
}
},
"horror": {
"core_promise": "Dread, fear, and confrontation with the threatening",
"required_elements": ["Vulnerability establishment", "Dread building", "Rules or violations"],
"categories": {
"dread_elements": [
"Something wrong with reflections",
"Animals behaving strangely",
"Children know something adults don't",
"Technology fails at worst moment",
"Help is unreachable",
"Rules that must be followed",
"Something familiar becomes unfamiliar",
"Evidence of previous victims",
"The thing knows their name",
"It was there the whole time",
"They're already infected or marked",
"The 'safe place' isn't",
"What they thought protected them doesn't",
"It's patient; it can wait",
"They've been here before"
],
"vulnerability_types": [
"Isolation from help or escape",
"Physical limitation (injured, exhausted, impaired)",
"Knowledge that makes it worse",
"Moral constraint that limits options",
"Dependency on something unreliable",
"Responsibility for someone more vulnerable",
"Attachment to place or object they cannot leave",
"Condition that worsens with stress",
"Secret that the threat could expose",
"Trauma that the situation echoes",
"Skill set useless against this specific threat",
"Belief system that prevents seeing clearly",
"Relationship that requires staying",
"Obligation that cannot be abandoned",
"Identity tied to confronting rather than fleeing"
],
"rule_structures": [
"It can only enter if invited",
"It can only see movement",
"It feeds on specific emotion",
"It cannot cross a particular boundary",
"It must follow ritual constraints",
"It is dormant until triggered",
"It can only take those who are alone",
"It grows stronger with attention",
"It weakens in specific conditions",
"It cannot harm those who do not believe",
"It obeys ancient bargains",
"It is bound to cycles of time",
"It must answer questions truthfully",
"It cannot directly lie but can deceive",
"It is limited to a specific location"
],
"escalation_beats": [
"First sign dismissed as imagination",
"Second sign shared but not believed",
"Evidence that should not exist",
"Disappearance or change in someone trusted",
"Discovery that this has happened before",
"Realization that it knows about them",
"Loss of exit or communication",
"Violation of a rule that was supposed to protect",
"Sacrifice that doesn't work as intended",
"Survivor who is not what they seem",
"Moment when it could have ended but didn't",
"Discovery that they were chosen",
"Rules changing without warning",
"Ally becoming threat",
"Final confrontation with diminished resources"
]
}
},
"mystery": {
"core_promise": "Curiosity about unknown facts through investigation",
"required_elements": ["Fair-play clues", "Investigation structure", "Revelation payoff"],
"categories": {
"clues": [
"Inconsistent timeline in witness statement",
"Object in wrong location",
"Missing item that should be present",
"Reaction that doesn't match the situation",
"Financial anomaly",
"Repeated 'coincidence'",
"Dog that didn't bark",
"Alibi too perfect",
"Physical evidence contradicts testimony",
"Someone lies about something trivial",
"Background detail only expert would notice",
"Victim's recent behavior change",
"Connection between unrelated people",
"Time-sensitive action taken too quickly",
"Knowledge of something not yet revealed"
],
"red_herrings": [
"Person with means but no motive",
"Coincidence that looks like pattern",
"True fact that misleads",
"Character who lies about something unrelated",
"Evidence from different crime entirely",
"Suspect with obvious but wrong motive",
"Obvious solution that doesn't quite fit",
"Person acting suspiciously for innocent reason",
"Evidence planted by third party",
"Pattern that's real but irrelevant",
"Expert whose expertise misleads",
"Confession that's false",
"Alibi that's too weak to be fabricated",
"Motive that exists but wasn't acted on",
"Timing that suggests but doesn't prove"
],
"revelation_structures": [
"Final piece makes all others make sense",
"Truth was visible from the start, meaning hidden",
"Clue reader knows, detective doesn't",
"Assumption everyone made was wrong",
"Simple explanation for complex appearance",
"Complex explanation for simple appearance",
"Motive was personal, appeared professional",
"Motive was professional, appeared personal",
"Two separate mysteries were one",
"One mystery was actually two",
"Victim was also perpetrator",
"Perpetrator was also victim",
"Witness saw correctly but interpreted wrong",
"Expert was right but missed the point",
"The obvious suspect did it, but not the obvious way"
],
"investigation_approaches": [
"Interview witnesses in different order",
"Re-examine crime scene with new question",
"Check records no one thought relevant",
"Find the person no one thought to ask",
"Verify the thing everyone assumed",
"Look for what isn't there",
"Map the timeline precisely",
"Follow the money",
"Check the alibi everyone accepted",
"Look at who benefits from confusion",
"Find the pattern in the exceptions",
"Ask why this victim, this time, this place",
"Identify what changed recently",
"Find who knew what when",
"Look for the door that doesn't fit"
]
}
},
"thriller": {
"core_promise": "Tension through immediate danger and high stakes",
"required_elements": ["Time pressure", "High stakes", "Power balance shifts"],
"categories": {
"ticking_clocks": [
"Hostage execution deadline",
"Bomb timer",
"Disease progression",
"Weather window closing",
"Arriving backup or reinforcements",
"Security system rebooting",
"Evidence being destroyed",
"Witness fleeing the country",
"Financial transaction completing",
"Building scheduled for demolition",
"Power source depleting",
"Cover story about to fall apart",
"Someone's patience running out",
"Natural disaster approaching",
"Medicine supply running low"
],
"stakes_types": [
"Personal: loved one in direct danger",
"Professional: career or reputation at risk",
"Global: many lives depend on outcome",
"Existential: way of life threatened",
"Moral: soul or integrity on the line",
"Historical: outcome determines what's remembered",
"Relational: trust that cannot be rebuilt if broken",
"Physical: body will not recover if failed",
"Mental: sanity depends on resolution",
"Legacy: what gets passed to next generation",
"Secret: exposure changes everything",
"Promise: word given that must be kept",
"Debt: obligation that will be collected",
"Sacrifice: someone already paid; cannot be wasted",
"Last chance: no opportunity for retry"
],
"power_shifts": [
"Hunter becomes hunted",
"Ally revealed as enemy",
"Resource suddenly depleted or destroyed",
"Safe house compromised",
"Backup plan discovered by opposition",
"Leverage evaporates",
"Information becomes outdated",
"Weapon becomes liability",
"Advantage becomes trap",
"Rescue becomes capture",
"Victory becomes bait",
"Escape becomes dead end",
"Trust becomes vulnerability",
"Strength becomes weakness in new context",
"Solution creates new problem"
],
"narrow_escapes": [
"Exit appears just as trap closes",
"Disguise holds for exactly long enough",
"Distraction works once and won't again",
"Ally arrives at last possible moment",
"Bluff is called but works",
"Plan B succeeds where Plan A failed",
"Enemy makes one small mistake",
"Forgotten skill proves useful",
"Weakness becomes unexpected advantage",
"Environment shifts in protagonist's favor",
"Pursuer is delayed by unrelated obstacle",
"Information arrives just in time",
"Betrayal is discovered before completion",
"Backup the enemy didn't know about",
"Sacrifice creates necessary window"
]
}
},
"humor": {
"core_promise": "Entertainment through comedy, amusement, and subverted expectations",
"required_elements": ["Incongruity", "Subversion", "Timing"],
"categories": {
"incongruity_sources": [
"Serious person in absurd situation",
"Absurd person in serious situation",
"Cultural collision between incompatible norms",
"Scale mismatch: cosmic stakes, petty execution",
"Scale mismatch: petty stakes, heroic execution",
"Competence mismatch: expert fails at basics",
"Competence mismatch: amateur succeeds where experts failed",
"Formality applied to informal situation",
"Informality applied to formal situation",
"Technical precision about ridiculous subject",
"Emotional response mismatched to trigger",
"Preparation for wrong problem",
"Solution that's worse than problem",
"Explanation that raises more questions",
"Expertise that's useless in context"
],
"subversion_types": [
"Expected tragedy becomes farce",
"Hero speech delivered to wrong audience",
"Villain monologue interrupted by mundane",
"Dramatic reveal no one cares about",
"Training montage for wrong skill",
"Romantic moment derailed by practicality",
"Threat that turns out to be harmless",
"Harmless thing that turns out to be threat",
"Prophecy fulfilled in literal but useless way",
"Preparation that addresses every concern but the real one",
"Sacrifice that wasn't necessary",
"Expert advice that's technically correct but useless",
"Lesson learned that doesn't apply",
"Transformation that changes nothing important",
"Victory that creates worse problem"
],
"timing_patterns": [
"Rule of three: setup, reinforce, break",
"Callback after long delay",
"Immediate undercut of serious moment",
"Slow burn then sudden payoff",
"Interruption at worst possible moment",
"Perfectly wrong timing",
"Anticipation built then deflated",
"Reaction before event",
"Delayed realization",
"Simultaneous discovery and consequence",
"Pause that makes the obvious funnier",
"Speed that makes the terrible funnier",
"Repetition that shouldn't still be funny but is",
"Pattern established then broken at wrong moment",
"Timing that was perfect but for wrong reason"
]
}
},
"relationship": {
"core_promise": "Emotional investment in interpersonal connections",
"required_elements": ["Meaningful obstacle", "Connection progression", "Resolution of tension"],
"categories": {
"obstacles": [
"Competing for same position or goal",
"Past history they've hidden",
"Class or status difference",
"Geographic separation",
"Promised to someone else",
"Professional ethics forbid it",
"One is the other's superior",
"Mutual friend makes it awkward",
"Bad timing: each ready at different times",
"Misunderstanding that compounds",
"External expectations from family or community",
"One is keeping a secret",
"History of bad relationships creates walls",
"They want different futures",
"Pride prevents admitting feelings"
],
"connection_progressions": [
"Forced proximity leads to reluctant alliance leads to genuine bond",
"Misunderstanding leads to clarification leads to deeper understanding",
"Competition leads to respect leads to collaboration",
"Distrust leads to testing leads to earned trust",
"Surface attraction leads to discovered depth leads to commitment",
"Shared enemy leads to alliance leads to chosen partnership",
"Obligation leads to care leads to love",
"Antagonism leads to curiosity leads to appreciation",
"Mentorship leads to equality leads to friendship",
"Need leads to dependence leads to interdependence",
"Projection leads to seeing clearly leads to loving truly",
"Fear leads to courage for other leads to courage for self",
"Loneliness leads to company leads to chosen isolation together",
"Pretense leads to slippage leads to truth",
"Transaction leads to gift leads to mutual giving"
],
"relationship_tests": [
"One must sacrifice something important",
"Secret revealed tests acceptance",
"External pressure to separate",
"Competing loyalty creates tension",
"Growth requires letting go",
"Crisis reveals character",
"Past returns to complicate present",
"Success threatens what worked in failure",
"Failure tests commitment",
"Third party offers easier alternative",
"One changes in way the other doesn't like",
"Silence must be broken",
"Forgiveness is required",
"Vulnerability must be chosen",
"Separate paths must be navigated together"
]
}
},
"drama": {
"core_promise": "Internal conflict and character transformation",
"required_elements": ["Value under pressure", "Difficult choice", "Transformation"],
"categories": {
"values_under_pressure": [
"Loyalty tested by new information",
"Honesty versus kindness",
"Duty versus personal desire",
"Principle versus pragmatism",
"Past commitment versus present reality",
"Safety versus growth",
"Independence versus connection",
"Forgiveness versus justice",
"Truth versus peace",
"Tradition versus progress",
"Self versus other",
"Control versus trust",
"Pride versus reconciliation",
"Ambition versus contentment",
"What's right versus what works"
],
"difficult_choices": [
"Both options require sacrifice",
"Right thing has wrong consequences",
"No option preserves everything valued",
"Choice defines who they become",
"Refusing to choose is a choice",
"Best choice is one they swore never to make",
"Easy choice is wrong; hard choice is right",
"Choice will be judged by people who don't understand",
"Choice betrays past self to become future self",
"Choice helps one by hurting another",
"Choice must be made with incomplete information",
"Choice must be made under pressure",
"Choice is irreversible",
"Choice must be lived with publicly",
"Choice alienates those who were trusted"
],
"transformation_catalysts": [
"Seeing self through others' eyes",
"Consequence of past choice arrives",
"Model of different path appears",
"Loss that clarifies priorities",
"Success that feels hollow",
"Failure that teaches truth",
"Child or student reflects back what was taught",
"Mentor falls from pedestal",
"Enemy earns respect",
"Friend reveals disappointment",
"Stranger shows what protagonist could become",
"Memory recontextualizes present",
"Body or circumstance forces new perspective",
"Hitting bottom provides foundation",
"Near-death illuminates life"
]
}
},
"issue": {
"core_promise": "Exploration of social or philosophical questions",
"required_elements": ["Multiple valid perspectives", "Complexity revealed", "Stakes for all sides"],
"categories": {
"issue_approaches": [
"Characters embody different positions",
"Same character changes position through experience",
"Position leads to unexpected consequence",
"Simple answer proves incomplete",
"Complex answer proves actionable",
"Historical parallel illuminates present",
"Personal stakes force abstract into concrete",
"Solution creates new problem",
"Opposite sides discover shared value",
"Victory feels hollow; defeat feels righteous",
"Compromise serves no one well",
"Extreme position contains truth",
"Moderate position contains cowardice",
"Facts support both sides differently",
"New information changes everything"
],
"perspective_types": [
"True believer with good reason",
"Pragmatist who compromises",
"Idealist who suffers for principles",
"Convert who switched sides",
"Agnostic who must choose anyway",
"Beneficiary who doesn't question",
"Victim who still believes in system",
"Reformer who works within",
"Revolutionary who works without",
"Inheritor of others' choices",
"Expert whose expertise limits understanding",
"Outsider who sees clearly",
"Insider who cannot see",
"Child who asks obvious questions",
"Elder who remembers different approach"
],
"complexity_reveals": [
"Solution creates new problem",
"Opponent has valid point",
"Ally is right for wrong reasons",
"Enemy is wrong for right reasons",
"Right position, wrong method",
"Wrong position, right method",
"Historical parallel complicates",
"Unintended consequences outweigh intended",
"Success by wrong means is still wrong",
"Failure by right means is still failure",
"Scale changes the morality",
"Context changes the meaning",
"Who decides matters more than what's decided",
"Process matters as much as outcome",
"Both sides are right; both sides are wrong"
]
}
},
"ensemble": {
"core_promise": "Group dynamics drive the story",
"required_elements": ["Diverse skills or perspectives", "Group challenges", "Individual contributions to collective success"],
"categories": {
"role_distributions": [
"Leader, skeptic, loyalist, wildcard",
"Brains, brawn, heart, conscience",
"Veteran, rookie, specialist, generalist",
"Insider, outsider, mediator, provocateur",
"Optimist, pessimist, realist, dreamer",
"Planner, improviser, supporter, challenger",
"Old guard, new blood, bridge between",
"Theory, practice, documentation, critique",
"Builder, breaker, fixer, maintainer",
"Talker, listener, observer, actor",
"Local knowledge, outside perspective, synthesis",
"Technical skill, people skill, luck",
"By the book, against the book, writing new book",
"Past, present, future orientations",
"Risk-taker, risk-manager, risk-blind"
],
"group_challenges": [
"Requires multiple skills applied simultaneously",
"Individual solution would definitely fail",
"Trust must be established under time pressure",
"Conflicting approaches must integrate",
"Loss of one member changes everything",
"Secret among group threatens cohesion",
"External pressure reveals fractures",
"Success requires someone's sacrifice",
"Failure is blamed unevenly",
"Leadership must be contested then resolved",
"Resources must be allocated fairly under scarcity",
"Information must be shared that's dangerous to share",
"Vulnerability must be admitted to strangers",
"Past must be processed while present demands action",
"Each must do what only they can do"
],
"dynamic_tensions": [
"Leadership contested between two valid candidates",
"Past history between members creates friction",
"Resource allocation creates resentment",
"Values clash on acceptable methods",
"Individual glory versus group success",
"Speed versus thoroughness",
"Transparency versus operational security",
"Inclusion versus efficiency",
"Forgiveness versus accountability",
"Hierarchy versus collaboration",
"Mission versus members",
"Present danger versus future prevention",
"What worked before versus what's needed now",
"Whose expertise applies here",
"When to follow leader versus when to challenge"
]
}
},
"setting_elements": {
"_note": "These are setting elements, not elemental genres. Useful for sci-fi and fantasy settings that can house any genre.",
"scifi_tech_consequences": [
"Technology has unintended side effect",
"Someone uses it for purposes never intended",
"Dependency develops; what happens when it fails?",
"Creates new form of inequality",
"Changes meaning of fundamental concept (death, identity, privacy)",
"Black market emerges around it",
"People find workarounds to restrictions",
"Second-generation effects no one predicted",
"Works differently on edge cases",
"Interacts badly with older systems",
"Corporate or government control implications",
"Religious or ethical movements arise against it",
"Nostalgia movement rejects it",
"Ruins an existing industry or profession",
"Changes warfare or crime in unexpected ways"
],
"fantasy_magic_costs": [
"Physical exhaustion",
"Shortened lifespan",
"Loss of memories",
"Emotional numbness",
"Physical transformation",
"Attracts dangerous attention",
"Creates obligation to cosmic force",
"Damages relationships (people sense something wrong)",
"Requires morally questionable components",
"Each use makes next easier (addiction risk)",
"Echoes forward in time (consequences appear later)",
"Someone else pays the price",
"Takes away ability to do mundane version",
"Creates debt that will be collected",
"Visibility to entities you'd rather not see you"
]
}
}
#!/usr/bin/env -S deno run --allow-read
/**
* Genre Blend Helper
*
* Provides strategies for integrating secondary genres with a primary genre.
* Identifies potential conflicts, suggests proportion guidelines, and warns
* of common failure modes for specific combinations.
*
* Usage:
* deno run --allow-read genre-blend.ts mystery relationship
* deno run --allow-read genre-blend.ts thriller --secondary humor,wonder
* deno run --allow-read genre-blend.ts --combos # Show all notable combos
*/
interface GenreInfo {
name: string;
displayName: string;
promise: string;
pacing: "fast" | "moderate" | "slow";
tone: "light" | "neutral" | "dark";
focus: "external" | "internal" | "both";
}
interface BlendStrategy {
primary: string;
secondary: string;
compatibility: "excellent" | "good" | "challenging" | "difficult";
strategies: string[];
pitfalls: string[];
proportionGuideline: string;
examples: string[];
}
const GENRES: GenreInfo[] = [
{ name: "wonder", displayName: "Wonder", promise: "Awe and fascination", pacing: "slow", tone: "light", focus: "external" },
{ name: "idea", displayName: "Idea", promise: "Intellectual fascination", pacing: "slow", tone: "neutral", focus: "internal" },
{ name: "adventure", displayName: "Adventure", promise: "Excitement through challenges", pacing: "fast", tone: "neutral", focus: "external" },
{ name: "horror", displayName: "Horror", promise: "Dread and fear", pacing: "moderate", tone: "dark", focus: "both" },
{ name: "mystery", displayName: "Mystery", promise: "Curiosity about unknowns", pacing: "moderate", tone: "neutral", focus: "external" },
{ name: "thriller", displayName: "Thriller", promise: "Tension through danger", pacing: "fast", tone: "dark", focus: "external" },
{ name: "humor", displayName: "Humor", promise: "Entertainment and amusement", pacing: "fast", tone: "light", focus: "both" },
{ name: "relationship", displayName: "Relationship", promise: "Investment in connections", pacing: "slow", tone: "neutral", focus: "internal" },
{ name: "drama", displayName: "Drama", promise: "Internal transformation", pacing: "slow", tone: "neutral", focus: "internal" },
{ name: "issue", displayName: "Issue", promise: "Exploration of questions", pacing: "slow", tone: "neutral", focus: "both" },
{ name: "ensemble", displayName: "Ensemble", promise: "Group dynamics", pacing: "moderate", tone: "neutral", focus: "both" },
];
// Pre-defined notable combinations with specific guidance
const NOTABLE_COMBOS: BlendStrategy[] = [
{
primary: "mystery",
secondary: "relationship",
compatibility: "excellent",
strategies: [
"Investigation creates forced proximity that develops relationship",
"Relationship stakes raise mystery stakes (what if the suspect is the love interest?)",
"Clue-gathering scenes double as relationship-building scenes",
"Resolution of mystery enables resolution of relationship",
],
pitfalls: [
"Romance subplot pauses investigation momentum",
"Romantic tension overshadows mystery tension",
"Clues get lost in relationship conversations",
],
proportionGuideline: "70% mystery / 30% relationship - relationship beats in sequel scenes",
examples: ["Romantic suspense novels", "Cozy mysteries with ongoing romance arcs"],
},
{
primary: "thriller",
secondary: "horror",
compatibility: "excellent",
strategies: [
"Horror elements raise thriller stakes beyond physical danger",
"Dread underlies action sequences",
"Survival horror: thriller pacing with horror atmosphere",
"The threat is both immediate (thriller) and existential (horror)",
],
pitfalls: [
"Constant action prevents dread from building",
"Horror atmosphere breaks when protagonist is too capable",
"Switching between fast thriller and slow horror creates whiplash",
],
proportionGuideline: "60% thriller / 40% horror - alternate between action and dread sequences",
examples: ["Alien", "The Silence of the Lambs", "28 Days Later"],
},
{
primary: "thriller",
secondary: "humor",
compatibility: "challenging",
strategies: [
"Humor provides relief valve between tension peaks",
"Character humor (not situation comedy) maintains tension",
"Gallows humor under pressure feels authentic",
"Comic sidekick contrasts with serious protagonist",
],
pitfalls: [
"Humor undercuts tension at crucial moments",
"Tonal whiplash between scary and funny",
"Stakes feel unreal if characters joke too much",
],
proportionGuideline: "80% thriller / 20% humor - humor only in setup and aftermath, never at peak tension",
examples: ["Die Hard", "True Lies", "Mr. & Mrs. Smith"],
},
{
primary: "horror",
secondary: "humor",
compatibility: "difficult",
strategies: [
"Horror-comedy requires full commitment to both",
"Humor about the horror, not despite it",
"Characters can be funny; situation remains horrifying",
"Absurdist logic that's both funny and terrifying",
],
pitfalls: [
"Horror becomes campy rather than scary",
"Humor makes audience not care about characters",
"Switching between modes rather than blending them",
],
proportionGuideline: "50/50 - must be genuinely both, not alternating",
examples: ["Shaun of the Dead", "Tucker and Dale vs Evil", "What We Do in the Shadows"],
},
{
primary: "adventure",
secondary: "wonder",
compatibility: "excellent",
strategies: [
"Discovery moments punctuate action sequences",
"New locations provide both challenges and awe",
"Wonder is the reward for overcoming obstacles",
"Scale of environment creates both danger and beauty",
],
pitfalls: [
"Pausing for wonder kills momentum",
"Wonder becomes background scenery rather than emotional beat",
"Too much spectacle, not enough story",
],
proportionGuideline: "70% adventure / 30% wonder - wonder at act breaks and destination arrivals",
examples: ["Indiana Jones", "Journey to the Center of the Earth", "Avatar"],
},
{
primary: "mystery",
secondary: "horror",
compatibility: "good",
strategies: [
"Investigation uncovers increasingly disturbing truths",
"Clues hint at something the reader dreads discovering",
"Fair-play mystery rules apply to supernatural horror",
"The more they learn, the worse it gets",
],
pitfalls: [
"Horror elements make mystery feel unfair",
"Investigation pace too slow for horror urgency",
"Revelation of monster removes mystery",
],
proportionGuideline: "60% mystery / 40% horror - horror escalates as mystery deepens",
examples: ["True Detective S1", "The Name of the Rose", "In the Mouth of Madness"],
},
{
primary: "drama",
secondary: "issue",
compatibility: "excellent",
strategies: [
"Personal transformation reflects larger social issue",
"Character embodies one perspective, then grows",
"Private stakes illuminate public stakes",
"The issue is experienced, not debated",
],
pitfalls: [
"Character becomes mouthpiece for position",
"Issue overwhelms personal story",
"Preachiness replaces drama",
],
proportionGuideline: "70% drama / 30% issue - issue is context, drama is story",
examples: ["Philadelphia", "Erin Brockovich", "The Remains of the Day"],
},
{
primary: "ensemble",
secondary: "adventure",
compatibility: "excellent",
strategies: [
"Different characters excel at different challenges",
"Group must work together to overcome obstacles",
"Journey tests group cohesion",
"Individual skill moments within collective goal",
],
pitfalls: [
"Too many characters, not enough development",
"Adventure overshadows character dynamics",
"Group splits and story fragments",
],
proportionGuideline: "50/50 - adventure provides structure, ensemble provides heart",
examples: ["Lord of the Rings", "Guardians of the Galaxy", "Ocean's Eleven"],
},
{
primary: "idea",
secondary: "thriller",
compatibility: "good",
strategies: [
"Time pressure forces concept to be tested urgently",
"Stakes make abstract ideas concrete",
"Concept exploration happens during action",
"Idea implications are the source of danger",
],
pitfalls: [
"Idea exposition slows thriller pacing",
"Thriller urgency prevents thoughtful exploration",
"Concept gets simplified for action convenience",
],
proportionGuideline: "40% idea / 60% thriller - front-load concept, let implications drive action",
examples: ["Inception", "Arrival", "Ex Machina"],
},
{
primary: "relationship",
secondary: "drama",
compatibility: "excellent",
strategies: [
"Relationship tests character values",
"Internal change enables external connection",
"Partner challenges protagonist's false beliefs",
"Growth in self enables growth in relationship",
],
pitfalls: [
"One partner becomes tool for other's growth",
"Relationship problems feel manufactured",
"Internal and external arcs don't align",
],
proportionGuideline: "55% relationship / 45% drama - intertwined throughout",
examples: ["Pride and Prejudice", "When Harry Met Sally", "Marriage Story"],
},
];
function getGenreInfo(name: string): GenreInfo | undefined {
return GENRES.find(g => g.name === name.toLowerCase());
}
function assessCompatibility(primary: GenreInfo, secondary: GenreInfo): "excellent" | "good" | "challenging" | "difficult" {
let score = 0;
// Same pacing is easier
if (primary.pacing === secondary.pacing) score += 2;
else if (
(primary.pacing === "moderate") ||
(secondary.pacing === "moderate")
) score += 1;
// Compatible tone
if (primary.tone === secondary.tone) score += 2;
else if (primary.tone === "neutral" || secondary.tone === "neutral") score += 1;
else if (primary.tone !== secondary.tone) score -= 1; // light/dark clash
// Focus compatibility
if (primary.focus === secondary.focus) score += 1;
else if (primary.focus === "both" || secondary.focus === "both") score += 1;
if (score >= 4) return "excellent";
if (score >= 2) return "good";
if (score >= 0) return "challenging";
return "difficult";
}
function generateGenericStrategy(primary: GenreInfo, secondary: GenreInfo): BlendStrategy {
const compatibility = assessCompatibility(primary, secondary);
const strategies: string[] = [];
const pitfalls: string[] = [];
// Pacing strategies
if (primary.pacing === "fast" && secondary.pacing === "slow") {
strategies.push(`Use ${secondary.displayName} elements in setup and aftermath, not during ${primary.displayName} sequences`);
pitfalls.push(`${secondary.displayName} elements may slow ${primary.displayName} momentum`);
} else if (primary.pacing === "slow" && secondary.pacing === "fast") {
strategies.push(`${secondary.displayName} sequences can punctuate ${primary.displayName} contemplation`);
pitfalls.push(`${secondary.displayName} urgency may not allow ${primary.displayName} depth`);
}
// Tone strategies
if (primary.tone === "dark" && secondary.tone === "light") {
strategies.push("Use contrast deliberately - light moments make dark moments darker");
pitfalls.push("Tonal whiplash if transitions aren't managed");
} else if (primary.tone === "light" && secondary.tone === "dark") {
strategies.push("Dark elements can add unexpected depth to light story");
pitfalls.push("Dark elements may feel out of place if not integrated");
}
// Focus strategies
if (primary.focus === "external" && secondary.focus === "internal") {
strategies.push(`Use sequel scenes for ${secondary.displayName} while scene scenes drive ${primary.displayName}`);
} else if (primary.focus === "internal" && secondary.focus === "external") {
strategies.push(`External ${secondary.displayName} events can trigger internal ${primary.displayName} responses`);
}
// Generic strategies
strategies.push(`Ensure ${secondary.displayName} elements serve ${primary.displayName} promise, not compete with it`);
strategies.push(`Consider which POV or subplot carries ${secondary.displayName} elements`);
// Generic pitfalls
pitfalls.push(`${secondary.displayName} overwhelming ${primary.displayName} if proportions aren't maintained`);
pitfalls.push(`Reader confusion about which genre promise to expect`);
// Proportion guideline
let proportion: string;
if (compatibility === "excellent") {
proportion = `60% ${primary.displayName} / 40% ${secondary.displayName} - can integrate throughout`;
} else if (compatibility === "good") {
proportion = `70% ${primary.displayName} / 30% ${secondary.displayName} - ${secondary.displayName} in specific scenes`;
} else if (compatibility === "challenging") {
proportion = `80% ${primary.displayName} / 20% ${secondary.displayName} - ${secondary.displayName} as accent only`;
} else {
proportion = `85% ${primary.displayName} / 15% ${secondary.displayName} - requires careful craft to blend`;
}
return {
primary: primary.displayName,
secondary: secondary.displayName,
compatibility,
strategies,
pitfalls,
proportionGuideline: proportion,
examples: [],
};
}
function getBlendStrategy(primaryName: string, secondaryName: string): BlendStrategy {
// Check for pre-defined notable combo
const notable = NOTABLE_COMBOS.find(
c => c.primary === primaryName && c.secondary === secondaryName
);
if (notable) return notable;
// Check reverse (some combos work both ways)
const reverseNotable = NOTABLE_COMBOS.find(
c => c.primary === secondaryName && c.secondary === primaryName
);
if (reverseNotable) {
// Adapt the reverse for this direction
return {
...reverseNotable,
primary: primaryName,
secondary: secondaryName,
proportionGuideline: reverseNotable.proportionGuideline.replace(
/(\d+)% .* \/ (\d+)% .*/,
`$2% ${getGenreInfo(primaryName)?.displayName} / $1% ${getGenreInfo(secondaryName)?.displayName}`
),
};
}
// Generate generic strategy
const primary = getGenreInfo(primaryName);
const secondary = getGenreInfo(secondaryName);
if (!primary || !secondary) {
throw new Error(`Unknown genre: ${!primary ? primaryName : secondaryName}`);
}
return generateGenericStrategy(primary, secondary);
}
function formatBlendReport(strategy: BlendStrategy): string {
const lines: string[] = [];
const compatIcon = {
excellent: "++++",
good: "+++",
challenging: "++",
difficult: "+",
}[strategy.compatibility];
lines.push(`# ${strategy.primary} + ${strategy.secondary} Blend\n`);
lines.push(`Compatibility: ${strategy.compatibility.toUpperCase()} ${compatIcon}\n`);
lines.push(`## Proportion Guideline`);
lines.push(`${strategy.proportionGuideline}\n`);
lines.push("## Integration Strategies");
for (const s of strategy.strategies) {
lines.push(` - ${s}`);
}
lines.push("");
lines.push("## Common Pitfalls");
for (const p of strategy.pitfalls) {
lines.push(` - ${p}`);
}
lines.push("");
if (strategy.examples.length > 0) {
lines.push("## Examples");
lines.push(` ${strategy.examples.join(", ")}\n`);
}
return lines.join("\n");
}
function formatMultiBlendReport(primary: string, secondaries: string[]): string {
const lines: string[] = [];
const primaryInfo = getGenreInfo(primary);
lines.push(`# Multi-Genre Blend: ${primaryInfo?.displayName}\n`);
lines.push(`Primary genre: **${primaryInfo?.displayName}** (${primaryInfo?.promise})\n`);
lines.push(`Secondary genres: ${secondaries.map(s => getGenreInfo(s)?.displayName).join(", ")}\n`);
// Overall proportion guidance
const secondaryCount = secondaries.length;
const primaryPercent = Math.max(50, 80 - (secondaryCount * 10));
const secondaryPercent = Math.floor((100 - primaryPercent) / secondaryCount);
lines.push("## Overall Proportion");
lines.push(` ${primaryInfo?.displayName}: ${primaryPercent}%`);
for (const s of secondaries) {
lines.push(` ${getGenreInfo(s)?.displayName}: ~${secondaryPercent}%`);
}
lines.push("");
lines.push("## Individual Blend Analysis\n");
for (const secondary of secondaries) {
const strategy = getBlendStrategy(primary, secondary);
lines.push(`### + ${strategy.secondary}`);
lines.push(`Compatibility: ${strategy.compatibility}`);
lines.push(`Key strategy: ${strategy.strategies[0]}`);
lines.push(`Watch for: ${strategy.pitfalls[0]}\n`);
}
// Interaction warnings
lines.push("## Multi-Secondary Considerations");
lines.push(" - Each secondary genre should serve the primary, not each other");
lines.push(" - Consider assigning secondary genres to different POVs or subplots");
lines.push(" - More genres = more complexity = higher craft requirement");
lines.push(" - If overwhelmed, cut to one secondary genre\n");
return lines.join("\n");
}
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Genre Blend Helper - Secondary genre integration strategies
Usage:
deno run --allow-read genre-blend.ts <primary> <secondary>
deno run --allow-read genre-blend.ts <primary> --secondary <g1>,<g2>
deno run --allow-read genre-blend.ts --combos
deno run --allow-read genre-blend.ts --matrix
Options:
--secondary Specify multiple secondary genres (comma-separated)
--combos Show all pre-defined notable combinations
--matrix Show compatibility matrix for all genre pairs
--json Output as JSON
Genres:
wonder, idea, adventure, horror, mystery, thriller,
humor, relationship, drama, issue, ensemble
`);
Deno.exit(0);
}
const jsonOutput = args.includes("--json");
// Show all notable combos
if (args.includes("--combos")) {
if (jsonOutput) {
console.log(JSON.stringify(NOTABLE_COMBOS, null, 2));
} else {
console.log("# Notable Genre Combinations\n");
for (const combo of NOTABLE_COMBOS) {
console.log(`## ${combo.primary} + ${combo.secondary} (${combo.compatibility})`);
console.log(` ${combo.proportionGuideline}`);
if (combo.examples.length > 0) {
console.log(` Examples: ${combo.examples.join(", ")}`);
}
console.log("");
}
}
Deno.exit(0);
}
// Show compatibility matrix
if (args.includes("--matrix")) {
console.log("# Genre Compatibility Matrix\n");
console.log(" " + GENRES.map(g => g.name.substring(0, 4).padEnd(5)).join(" "));
for (const g1 of GENRES) {
const row = g1.name.substring(0, 6).padEnd(7);
const cells = GENRES.map(g2 => {
if (g1.name === g2.name) return " - ";
const compat = assessCompatibility(g1, g2);
const icon = { excellent: " ++++ ", good: " +++ ", challenging: " ++ ", difficult: " + " }[compat];
return icon;
});
console.log(row + cells.join(""));
}
console.log("\n++++ excellent | +++ good | ++ challenging | + difficult");
Deno.exit(0);
}
// Multiple secondary genres
if (args.includes("--secondary")) {
const secondaryIndex = args.indexOf("--secondary");
const secondaries = args[secondaryIndex + 1]?.split(",").map(s => s.trim()) || [];
// Find primary (first non-flag arg)
let primary: string | undefined;
for (const arg of args) {
if (!arg.startsWith("--") && GENRES.some(g => g.name === arg)) {
primary = arg;
break;
}
}
if (!primary) {
console.error("Error: Specify a primary genre");
Deno.exit(1);
}
for (const s of secondaries) {
if (!GENRES.some(g => g.name === s)) {
console.error(`Error: Unknown genre "${s}"`);
Deno.exit(1);
}
}
if (jsonOutput) {
const strategies = secondaries.map(s => getBlendStrategy(primary!, s));
console.log(JSON.stringify({ primary, secondaries, strategies }, null, 2));
} else {
console.log(formatMultiBlendReport(primary, secondaries));
}
Deno.exit(0);
}
// Simple two-genre blend
const genreArgs = args.filter(a => !a.startsWith("--") && GENRES.some(g => g.name === a));
if (genreArgs.length < 2) {
console.error("Error: Specify primary and secondary genres");
console.error("Usage: genre-blend.ts <primary> <secondary>");
Deno.exit(1);
}
const [primary, secondary] = genreArgs;
const strategy = getBlendStrategy(primary, secondary);
if (jsonOutput) {
console.log(JSON.stringify(strategy, null, 2));
} else {
console.log(formatBlendReport(strategy));
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Genre Check Analyzer
*
* Analyzes text for genre-specific elements and patterns.
* Can check against a specified genre or attempt to detect likely genre.
*
* Usage:
* deno run --allow-read genre-check.ts --genre mystery scene.txt
* deno run --allow-read genre-check.ts --analyze "Synopsis text here"
* deno run --allow-read genre-check.ts --text "The detective arrived..." --genre mystery
*/
interface GenrePatterns {
name: string;
displayName: string;
promise: string;
required: string[];
patterns: Record<string, RegExp[]>;
}
interface GenreAnalysis {
genre: string;
matchScore: number;
totalMatches: number;
categoryBreakdown: Record<string, { count: number; samples: string[] }>;
presentElements: string[];
missingElements: string[];
issues: string[];
recommendations: string[];
}
interface MultiGenreAnalysis {
wordCount: number;
likelyPrimaryGenre: string;
genreScores: Record<string, number>;
detailedAnalysis: GenreAnalysis;
}
// Genre-specific pattern definitions
const GENRE_PATTERNS: GenrePatterns[] = [
{
name: "wonder",
displayName: "Wonder",
promise: "Awe and fascination with the unfamiliar",
required: ["scale/scope indicators", "discovery moments", "perspective shifts"],
patterns: {
scale_scope: [
/\b(vast|immense|infinite|endless|boundless|cosmic|universal)\b/gi,
/\b(horizon|expanse|beyond|stretching|spanning)\b/gi,
/\b(ancient|eons|millennia|timeless|eternal)\b/gi,
],
discovery: [
/\b(discover|discovered|discovering|revelation|reveal|revealed)\b/gi,
/\b(first time|never before|unprecedented|unknown|unexplored)\b/gi,
/\b(realize|realized|understanding|comprehend)\b/gi,
],
awe: [
/\b(awe|wonder|amazement|astonishment|marvel)\b/gi,
/\b(breathtaking|magnificent|spectacular|extraordinary)\b/gi,
/\b(impossible|unbelievable|inconceivable)\b/gi,
],
},
},
{
name: "idea",
displayName: "Idea",
promise: "Intellectual fascination with concepts",
required: ["central concept", "implication exploration", "hypothesis testing"],
patterns: {
concepts: [
/\b(what if|imagine if|suppose|consider|hypothesis)\b/gi,
/\b(theory|concept|principle|paradigm|framework)\b/gi,
/\b(implications|consequences|ramifications|effects)\b/gi,
],
exploration: [
/\b(therefore|thus|hence|consequently|as a result)\b/gi,
/\b(leads to|results in|causes|means that)\b/gi,
/\b(consider|examine|explore|investigate|analyze)\b/gi,
],
questioning: [
/\b(why|how|what does this mean|what happens when)\b/gi,
/\b(question|wonder|ponder|contemplate)\b/gi,
/\b(paradox|contradiction|dilemma|puzzle)\b/gi,
],
},
},
{
name: "adventure",
displayName: "Adventure",
promise: "Excitement through challenges and journeys",
required: ["journey/quest", "escalating obstacles", "resource pressure"],
patterns: {
journey: [
/\b(journey|quest|expedition|voyage|trek|travel)\b/gi,
/\b(destination|goal|objective|mission|target)\b/gi,
/\b(map|path|route|trail|way forward)\b/gi,
],
obstacles: [
/\b(obstacle|challenge|barrier|block|hindrance)\b/gi,
/\b(climb|cross|navigate|overcome|survive)\b/gi,
/\b(dangerous|treacherous|difficult|impossible|deadly)\b/gi,
],
resources: [
/\b(supplies|provisions|rations|equipment|gear)\b/gi,
/\b(running low|depleted|exhausted|scarce|limited)\b/gi,
/\b(water|food|fuel|ammunition|medicine)\b/gi,
],
},
},
{
name: "horror",
displayName: "Horror",
promise: "Dread and confrontation with the threatening",
required: ["vulnerability", "dread building", "rules or violations"],
patterns: {
dread: [
/\b(dread|terror|horror|fear|panic|fright)\b/gi,
/\b(creeping|lurking|watching|waiting|stalking)\b/gi,
/\b(wrong|off|strange|unnatural|disturbing)\b/gi,
],
vulnerability: [
/\b(alone|isolated|trapped|helpless|powerless)\b/gi,
/\b(dark|darkness|shadow|shadows|night)\b/gi,
/\b(no escape|nowhere to run|can't get out)\b/gi,
],
threat: [
/\b(thing|creature|it|them|monster|entity)\b/gi,
/\b(behind|watching|following|closer|approaching)\b/gi,
/\b(scream|blood|death|die|kill|dead)\b/gi,
],
},
},
{
name: "mystery",
displayName: "Mystery",
promise: "Curiosity about unknown facts",
required: ["clues", "investigation", "revelation"],
patterns: {
clues: [
/\b(clue|evidence|proof|trace|sign|indication)\b/gi,
/\b(noticed|observed|spotted|found|discovered)\b/gi,
/\b(strange|odd|unusual|inconsistent|suspicious)\b/gi,
],
investigation: [
/\b(investigate|examine|inspect|analyze|study)\b/gi,
/\b(question|interview|interrogate|ask)\b/gi,
/\b(detective|investigator|officer|inspector)\b/gi,
],
revelation: [
/\b(truth|answer|solution|explanation|revelation)\b/gi,
/\b(realize|understand|figure out|piece together)\b/gi,
/\b(who|what|why|how|when|where)\b/gi,
],
},
},
{
name: "thriller",
displayName: "Thriller",
promise: "Tension through immediate danger",
required: ["time pressure", "high stakes", "power shifts"],
patterns: {
time_pressure: [
/\b(deadline|timer|countdown|clock|hours|minutes)\b/gi,
/\b(hurry|rush|quickly|fast|immediately|now)\b/gi,
/\b(running out|too late|before|until)\b/gi,
],
stakes: [
/\b(life|death|lives|die|kill|survive|save)\b/gi,
/\b(million|billion|everything|everyone|world)\b/gi,
/\b(hostage|bomb|attack|threat|danger)\b/gi,
],
tension: [
/\b(chase|pursue|hunt|flee|escape|run)\b/gi,
/\b(catch|caught|trapped|cornered|surrounded)\b/gi,
/\b(heart|pulse|breath|sweat|adrenaline)\b/gi,
],
},
},
{
name: "humor",
displayName: "Humor",
promise: "Entertainment through comedy",
required: ["incongruity", "subversion", "timing"],
patterns: {
incongruity: [
/\b(absurd|ridiculous|ludicrous|preposterous)\b/gi,
/\b(unexpected|surprising|ironic|irony)\b/gi,
/\b(misunderstanding|confusion|mixed up|wrong)\b/gi,
],
subversion: [
/\b(actually|instead|but|however|turns out)\b/gi,
/\b(supposed to|meant to|should have|tried to)\b/gi,
/\b(unfortunately|somehow|accidentally|oops)\b/gi,
],
reaction: [
/\b(laugh|laughed|laughing|chuckle|giggle|snort)\b/gi,
/\b(smile|grin|smirk|amused|funny)\b/gi,
/\b(joke|punchline|wit|clever|deadpan)\b/gi,
],
},
},
{
name: "relationship",
displayName: "Relationship",
promise: "Investment in interpersonal connections",
required: ["meaningful obstacle", "connection progression", "resolution"],
patterns: {
connection: [
/\b(love|loved|loving|heart|soul|feeling)\b/gi,
/\b(together|between|us|we|our|relationship)\b/gi,
/\b(trust|care|understand|know|accept)\b/gi,
],
obstacle: [
/\b(can't|couldn't|shouldn't|forbidden|impossible)\b/gi,
/\b(between us|apart|separate|distance|barrier)\b/gi,
/\b(secret|hide|hiding|lie|lied|truth)\b/gi,
],
progression: [
/\b(first|began|started|moment|when we)\b/gi,
/\b(closer|deeper|more|growing|falling)\b/gi,
/\b(finally|at last|admitted|confessed|realized)\b/gi,
],
},
},
{
name: "drama",
displayName: "Drama",
promise: "Internal conflict and transformation",
required: ["value under pressure", "difficult choice", "transformation"],
patterns: {
internal_conflict: [
/\b(torn|conflicted|struggled|wrestling|battling)\b/gi,
/\b(should|must|have to|need to|can't)\b/gi,
/\b(wrong|right|choice|decide|decision)\b/gi,
],
values: [
/\b(believe|principle|value|honor|integrity|duty)\b/gi,
/\b(sacrifice|cost|price|give up|lose)\b/gi,
/\b(worth|meaning|matter|important|everything)\b/gi,
],
transformation: [
/\b(change|changed|different|become|became)\b/gi,
/\b(realize|understand|see now|finally)\b/gi,
/\b(was|used to|before|now|anymore)\b/gi,
],
},
},
{
name: "issue",
displayName: "Issue",
promise: "Exploration of complex questions",
required: ["multiple perspectives", "complexity revealed", "stakes for all sides"],
patterns: {
perspectives: [
/\b(side|perspective|view|viewpoint|opinion)\b/gi,
/\b(some believe|others think|they say|we argue)\b/gi,
/\b(but|however|although|yet|still|nevertheless)\b/gi,
],
complexity: [
/\b(complicated|complex|nuanced|layered|difficult)\b/gi,
/\b(not simple|not easy|gray area|both|neither)\b/gi,
/\b(depends|context|situation|circumstances)\b/gi,
],
stakes: [
/\b(rights|justice|freedom|equality|future)\b/gi,
/\b(society|community|people|everyone|generation)\b/gi,
/\b(consequence|impact|effect|change|matter)\b/gi,
],
},
},
{
name: "ensemble",
displayName: "Ensemble",
promise: "Group dynamics drive the story",
required: ["diverse skills", "group challenges", "collective success"],
patterns: {
group: [
/\b(team|group|crew|squad|party|band)\b/gi,
/\b(together|all of us|everyone|each|we)\b/gi,
/\b(member|role|part|contribution|place)\b/gi,
],
diversity: [
/\b(different|unique|special|each|own way)\b/gi,
/\b(skill|ability|talent|expertise|strength)\b/gi,
/\b(leader|expert|specialist|only one who)\b/gi,
],
cooperation: [
/\b(work together|combine|coordinate|cooperate)\b/gi,
/\b(trust|rely|depend|count on|need each other)\b/gi,
/\b(plan|strategy|formation|position|cover)\b/gi,
],
},
},
];
function countMatches(text: string, patterns: RegExp[]): { count: number; samples: string[] } {
const matches: string[] = [];
for (const pattern of patterns) {
const found = text.match(pattern);
if (found) {
matches.push(...found.slice(0, 3));
}
}
const unique = [...new Set(matches.map(m => m.toLowerCase()))];
return {
count: unique.length,
samples: unique.slice(0, 5),
};
}
function analyzeForGenre(text: string, genre: GenrePatterns): GenreAnalysis {
const categoryBreakdown: Record<string, { count: number; samples: string[] }> = {};
let totalMatches = 0;
for (const [category, patterns] of Object.entries(genre.patterns)) {
const result = countMatches(text, patterns);
categoryBreakdown[category] = result;
totalMatches += result.count;
}
// Calculate match score (0-100)
const categoryCount = Object.keys(genre.patterns).length;
const maxExpectedPerCategory = 5;
const maxScore = categoryCount * maxExpectedPerCategory;
const matchScore = Math.min(100, Math.round((totalMatches / maxScore) * 100));
// Determine present and missing elements
const presentElements: string[] = [];
const missingElements: string[] = [];
for (const [category, result] of Object.entries(categoryBreakdown)) {
if (result.count > 0) {
presentElements.push(category.replace(/_/g, " "));
} else {
missingElements.push(category.replace(/_/g, " "));
}
}
// Generate issues and recommendations
const issues: string[] = [];
const recommendations: string[] = [];
if (matchScore < 20) {
issues.push(`Very few ${genre.displayName} elements detected`);
recommendations.push(`Consider adding ${genre.required.join(", ")}`);
} else if (matchScore < 40) {
issues.push(`${genre.displayName} elements present but weak`);
}
if (missingElements.length > 0) {
issues.push(`Missing ${genre.displayName} categories: ${missingElements.join(", ")}`);
recommendations.push(`Strengthen ${missingElements[0]} indicators`);
}
// Check for genre-specific issues
if (genre.name === "mystery" && categoryBreakdown.clues?.count === 0) {
issues.push("Mystery without detectable clues");
recommendations.push("Plant fair-play clues the reader could notice");
}
if (genre.name === "thriller" && categoryBreakdown.time_pressure?.count === 0) {
issues.push("Thriller without time pressure");
recommendations.push("Add a ticking clock or deadline");
}
if (genre.name === "horror" && categoryBreakdown.vulnerability?.count === 0) {
issues.push("Horror without established vulnerability");
recommendations.push("Establish why characters cannot simply escape");
}
return {
genre: genre.displayName,
matchScore,
totalMatches,
categoryBreakdown,
presentElements,
missingElements,
issues,
recommendations,
};
}
function analyzeMultiGenre(text: string): MultiGenreAnalysis {
const words = text.split(/\s+/).filter(w => w.length > 0);
const genreScores: Record<string, number> = {};
let bestGenre = GENRE_PATTERNS[0];
let bestScore = 0;
for (const genre of GENRE_PATTERNS) {
const analysis = analyzeForGenre(text, genre);
genreScores[genre.displayName] = analysis.matchScore;
if (analysis.matchScore > bestScore) {
bestScore = analysis.matchScore;
bestGenre = genre;
}
}
return {
wordCount: words.length,
likelyPrimaryGenre: bestGenre.displayName,
genreScores,
detailedAnalysis: analyzeForGenre(text, bestGenre),
};
}
function formatReport(analysis: GenreAnalysis): string {
const lines: string[] = [];
lines.push(`# ${analysis.genre} Genre Analysis\n`);
lines.push(`## Match Score: ${analysis.matchScore}/100\n`);
lines.push("## Category Breakdown");
for (const [category, result] of Object.entries(analysis.categoryBreakdown)) {
const status = result.count > 0 ? "+" : "-";
const samples = result.samples.length > 0 ? `: ${result.samples.join(", ")}` : "";
lines.push(` ${status} ${category.replace(/_/g, " ")} (${result.count})${samples}`);
}
lines.push("");
if (analysis.presentElements.length > 0) {
lines.push(`## Present Elements`);
lines.push(` ${analysis.presentElements.join(", ")}\n`);
}
if (analysis.missingElements.length > 0) {
lines.push(`## Missing Elements`);
lines.push(` ${analysis.missingElements.join(", ")}\n`);
}
if (analysis.issues.length > 0) {
lines.push("## Issues");
for (const issue of analysis.issues) {
lines.push(` - ${issue}`);
}
lines.push("");
}
if (analysis.recommendations.length > 0) {
lines.push("## Recommendations");
for (const rec of analysis.recommendations) {
lines.push(` - ${rec}`);
}
lines.push("");
}
return lines.join("\n");
}
function formatMultiGenreReport(analysis: MultiGenreAnalysis): string {
const lines: string[] = [];
lines.push("# Multi-Genre Analysis\n");
lines.push(`Word count: ${analysis.wordCount}`);
lines.push(`Likely primary genre: **${analysis.likelyPrimaryGenre}**\n`);
lines.push("## Genre Scores");
const sorted = Object.entries(analysis.genreScores).sort((a, b) => b[1] - a[1]);
for (const [genre, score] of sorted) {
const bar = "=".repeat(Math.floor(score / 5));
lines.push(` ${genre.padEnd(15)} ${bar} ${score}`);
}
lines.push("");
lines.push("## Primary Genre Details\n");
lines.push(formatReport(analysis.detailedAnalysis));
return lines.join("\n");
}
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Genre Check Analyzer - Detect genre elements in text
Usage:
deno run --allow-read genre-check.ts --genre <genre> <file>
deno run --allow-read genre-check.ts --genre <genre> --text "text here"
deno run --allow-read genre-check.ts --analyze <file>
deno run --allow-read genre-check.ts --analyze --text "text here"
Modes:
--genre <g> Check text against specific genre
--analyze Auto-detect likely genre, score all
Options:
--text "..." Provide text inline instead of file
--json Output as JSON
--help Show this message
Genres:
wonder, idea, adventure, horror, mystery, thriller,
humor, relationship, drama, issue, ensemble
`);
Deno.exit(0);
}
const jsonOutput = args.includes("--json");
let text = "";
// Get text from file or --text
if (args.includes("--text")) {
const textIndex = args.indexOf("--text");
text = args[textIndex + 1] || "";
} else {
// Find file path (non-flag argument that's not a genre name)
const genreNames = GENRE_PATTERNS.map(g => g.name);
const genreIndex = args.indexOf("--genre");
const skipIndices = new Set<number>();
if (genreIndex !== -1) {
skipIndices.add(genreIndex);
skipIndices.add(genreIndex + 1);
}
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith("--") && !skipIndices.has(i) && !genreNames.includes(args[i])) {
try {
text = await Deno.readTextFile(args[i]);
break;
} catch {
// Not a file, continue
}
}
}
}
if (!text.trim()) {
console.error("Error: No text provided. Use --text or provide a file path.");
Deno.exit(1);
}
// Analyze mode (multi-genre)
if (args.includes("--analyze")) {
const analysis = analyzeMultiGenre(text);
if (jsonOutput) {
console.log(JSON.stringify(analysis, null, 2));
} else {
console.log(formatMultiGenreReport(analysis));
}
Deno.exit(0);
}
// Specific genre mode
if (args.includes("--genre")) {
const genreIndex = args.indexOf("--genre");
const genreName = args[genreIndex + 1]?.toLowerCase();
const genre = GENRE_PATTERNS.find(g => g.name === genreName);
if (!genre) {
console.error(`Error: Unknown genre "${genreName}"`);
console.error(`Available: ${GENRE_PATTERNS.map(g => g.name).join(", ")}`);
Deno.exit(1);
}
const analysis = analyzeForGenre(text, genre);
if (jsonOutput) {
console.log(JSON.stringify(analysis, null, 2));
} else {
console.log(formatReport(analysis));
}
Deno.exit(0);
}
console.error("Error: Specify --genre <genre> or --analyze");
Deno.exit(1);
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Genre Elements Generator
*
* Randomly selects genre-specific elements for creative entropy.
* Covers all 11 elemental genres from the Writing Excuses framework.
*
* Usage:
* deno run --allow-read genre-elements.ts mystery # Random mystery element
* deno run --allow-read genre-elements.ts thriller --category ticking_clocks
* deno run --allow-read genre-elements.ts horror --count 3
* deno run --allow-read genre-elements.ts --list # Show all genres/categories
* deno run --allow-read genre-elements.ts --combo mystery,thriller
*/
import { dirname, fromFileUrl, join } from "https://deno.land/std@0.208.0/path/mod.ts";
interface GenreData {
core_promise: string;
required_elements: string[];
categories: Record<string, string[]>;
}
interface DataFile {
_meta: {
description: string;
version: string;
genres: string[];
};
setting_elements?: Record<string, string[]>;
[key: string]: GenreData | Record<string, string[]> | { description: string; version: string; genres: string[] } | undefined;
}
function randomFrom<T>(arr: T[], count: number = 1): T[] {
const shuffled = [...arr].sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(count, arr.length));
}
async function loadData(): Promise<DataFile> {
const scriptDir = dirname(fromFileUrl(import.meta.url));
const dataPath = join(scriptDir, "..", "data", "genre-elements.json");
const text = await Deno.readTextFile(dataPath);
return JSON.parse(text);
}
function getGenres(data: DataFile): string[] {
return data._meta.genres;
}
function getGenreData(data: DataFile, genre: string): GenreData | null {
const genreData = data[genre];
if (genreData && "categories" in genreData) {
return genreData as GenreData;
}
return null;
}
function getAllElements(genreData: GenreData): string[] {
const all: string[] = [];
for (const items of Object.values(genreData.categories)) {
all.push(...items);
}
return all;
}
function formatGenreInfo(genre: string, genreData: GenreData): string {
const lines: string[] = [];
lines.push(`\n## ${genre.charAt(0).toUpperCase() + genre.slice(1)}`);
lines.push(`Core promise: ${genreData.core_promise}`);
lines.push(`Required elements: ${genreData.required_elements.join(", ")}`);
lines.push(`Categories:`);
for (const [cat, items] of Object.entries(genreData.categories)) {
lines.push(` - ${cat} (${items.length} items)`);
}
return lines.join("\n");
}
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Genre Elements Generator - Random elements for each elemental genre
Usage:
deno run --allow-read genre-elements.ts <genre> # Random element
deno run --allow-read genre-elements.ts <genre> --category <cat> # From specific category
deno run --allow-read genre-elements.ts <genre> --count N # N random elements
deno run --allow-read genre-elements.ts --list # Show all genres
deno run --allow-read genre-elements.ts --list <genre> # Show genre categories
deno run --allow-read genre-elements.ts --combo <genre1>,<genre2> # One from each genre
deno run --allow-read genre-elements.ts --info <genre> # Genre requirements
Genres:
wonder, idea, adventure, horror, mystery, thriller,
humor, relationship, drama, issue, ensemble
Options:
--count N Return N random items
--category C Select from specific category within genre
--json Output as JSON
--list Show available genres or categories
--combo Generate one element from each specified genre
--info Show genre requirements and categories
--setting Include setting elements (scifi_tech, fantasy_magic)
`);
Deno.exit(0);
}
const data = await loadData();
const genres = getGenres(data);
const jsonOutput = args.includes("--json");
// List all genres
if (args.includes("--list")) {
const listIndex = args.indexOf("--list");
const specificGenre = args[listIndex + 1];
if (specificGenre && genres.includes(specificGenre)) {
const genreData = getGenreData(data, specificGenre);
if (genreData) {
console.log(`\nCategories for ${specificGenre}:\n`);
for (const [cat, items] of Object.entries(genreData.categories)) {
console.log(` ${cat.padEnd(25)} (${items.length} items)`);
}
}
} else {
console.log("\nAvailable elemental genres:\n");
for (const genre of genres) {
const gd = getGenreData(data, genre);
if (gd) {
const totalItems = Object.values(gd.categories).reduce((sum, arr) => sum + arr.length, 0);
console.log(` ${genre.padEnd(15)} - ${gd.core_promise.substring(0, 50)}... (${totalItems} items)`);
}
}
console.log("\nSetting elements (not genres):");
if (data.setting_elements) {
for (const [key, items] of Object.entries(data.setting_elements)) {
if (key !== "_note") {
console.log(` ${key.padEnd(25)} (${(items as string[]).length} items)`);
}
}
}
}
Deno.exit(0);
}
// Show genre info
if (args.includes("--info")) {
const infoIndex = args.indexOf("--info");
const genre = args[infoIndex + 1];
if (!genre || !genres.includes(genre)) {
console.error("Error: Specify a valid genre with --info");
Deno.exit(1);
}
const genreData = getGenreData(data, genre);
if (genreData) {
console.log(formatGenreInfo(genre, genreData));
}
Deno.exit(0);
}
// Combo mode
if (args.includes("--combo")) {
const comboIndex = args.indexOf("--combo");
const comboGenres = args[comboIndex + 1]?.split(",") || [];
if (comboGenres.length === 0) {
console.error("Error: Specify genres with --combo (e.g., --combo mystery,thriller)");
Deno.exit(1);
}
const result: Record<string, string> = {};
for (const genre of comboGenres) {
const trimmed = genre.trim();
if (!genres.includes(trimmed)) {
console.error(`Error: Unknown genre "${trimmed}"`);
Deno.exit(1);
}
const genreData = getGenreData(data, trimmed);
if (genreData) {
const all = getAllElements(genreData);
result[trimmed] = randomFrom(all, 1)[0];
}
}
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log("\nGenre combo:\n");
for (const [genre, element] of Object.entries(result)) {
console.log(` ${genre}: ${element}`);
}
}
Deno.exit(0);
}
// Parse count
const countIndex = args.indexOf("--count");
const count = countIndex !== -1 ? parseInt(args[countIndex + 1]) || 1 : 1;
// Parse category
const categoryIndex = args.indexOf("--category");
const category = categoryIndex !== -1 ? args[categoryIndex + 1] : null;
// Find the genre (first non-flag argument)
const skipIndices = new Set<number>();
if (countIndex !== -1) {
skipIndices.add(countIndex);
skipIndices.add(countIndex + 1);
}
if (categoryIndex !== -1) {
skipIndices.add(categoryIndex);
skipIndices.add(categoryIndex + 1);
}
let genre: string | undefined;
for (let i = 0; i < args.length; i++) {
if (!args[i].startsWith("--") && !skipIndices.has(i)) {
genre = args[i];
break;
}
}
// Check for setting elements
if (args.includes("--setting")) {
const settingElements = data.setting_elements;
if (settingElements) {
const allSetting: string[] = [];
for (const [key, items] of Object.entries(settingElements)) {
if (key !== "_note" && Array.isArray(items)) {
allSetting.push(...items);
}
}
const selected = randomFrom(allSetting, count);
if (jsonOutput) {
console.log(JSON.stringify(selected, null, 2));
} else {
for (const item of selected) {
console.log(`- ${item}`);
}
}
}
Deno.exit(0);
}
if (!genre) {
console.error("Error: No genre specified. Use --list to see options.");
Deno.exit(1);
}
if (!genres.includes(genre)) {
console.error(`Error: Unknown genre "${genre}". Use --list to see options.`);
Deno.exit(1);
}
const genreData = getGenreData(data, genre);
if (!genreData) {
console.error(`Error: Could not load data for genre "${genre}"`);
Deno.exit(1);
}
let pool: string[];
if (category) {
if (!genreData.categories[category]) {
console.error(`Error: Unknown category "${category}" for genre "${genre}"`);
console.error(`Available categories: ${Object.keys(genreData.categories).join(", ")}`);
Deno.exit(1);
}
pool = genreData.categories[category];
} else {
pool = getAllElements(genreData);
}
const selected = randomFrom(pool, count);
if (jsonOutput) {
console.log(JSON.stringify(selected, null, 2));
} else {
if (count === 1) {
console.log(selected[0]);
} else {
for (const item of selected) {
console.log(`- ${item}`);
}
}
}
}
main();