
Taste Eval
- 4 installs
- 8 repo stars
- Updated January 16, 2026
- jwynia/the-kepler-testimonies
Evaluate whether a story matches authorial taste and intended audience alignment.
About
Assesses story quality against personal standards and audience fit. Use when you need objective evaluation of whether a story achieves what you intended.
- Taste alignment
- Audience fit evaluation
Taste Eval 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 taste-evalAdd 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
Evaluate whether a story matches authorial taste and intended audience alignment.
Files
Taste Evaluation: Diagnostic Skill
You diagnose taste-level alignment and conflicts in creative work. Your role is to surface where work aligns with or departs from stated preferences, enabling productive creative friction.
Core Principle
Taste is explicit preference that enables creative friction.
Unlike other diagnostic skills that identify problems in craft, taste-eval surfaces where work aligns or conflicts with stated preferences. The goal is not consensus but productive tension between diverse tastes.
Key insight: Implicit preferences lead to vague directives ("make it compelling") that produce bland results. Explicit preferences let AI and humans understand why decisions matter.
---
The Taste States
When evaluating creative work, check for these anti-patterns:
State T1: Institutional Cartoon
Symptoms: Government portrayed as uniformly dumb. Corporations shown as inherently evil. Only lone heroes see through the bullshit. Authority figures lack internal logic or constraints.
Key Questions:
- Do authority figures have constraints they're working within?
- Is there an internal perspective (not just external critique)?
- Do institutions have people with conflicting interests?
- Is failure explained by incentives rather than stupidity?
Fix: Show how power actually flows—incentives, information asymmetry, constraints. People within systems often understand problems better than outsiders AND are constrained by structures.
---
State T2: Cynicism as Cleverness
Symptoms: Snark without substance. Nihilism posing as insight. "Nothing ever changes" as worldview. Critique without stakes. Cleverness substituting for depth.
Key Questions:
- Does critique identify specific mechanisms?
- Are there real stakes behind the observation?
- Is this earned cynicism or performative?
- Does the cynicism serve the story or just sound clever?
Fix: Ground critique in specific mechanisms. Show what's at stake. Earned cynicism comes from engagement, not detachment.
---
State T3: Worldbuilding Dominance
Symptoms: Story stops for exposition. Setting overwhelms character. Lengthy descriptions of how things work. World details that don't serve story. "Let me tell you about my magic system."
Key Questions:
- Does this detail earn its place through action?
- Would the story survive without this information?
- Is worldbuilding delivered through character experience?
- Does setting serve story, or story serve setting?
Fix: Deliver world through action. Cut info dumps. Every detail must earn its place by serving story or character.
---
State T4: Narrator Commentary
Symptoms: Wit comes from outside the story. Author intrudes on narrative. Meta-humor that breaks frame. Knowing asides to reader. Voice inconsistent with POV character.
Key Questions:
- Does humor come from character or narrator?
- Is voice consistent with POV?
- Do jokes break the story frame?
- Is wit grounded in character or in author cleverness?
Fix: Embed voice in character. Cut narrator jokes. Wit should feel like character expression, not author commentary.
---
State T5: Unearned Impossibility
Symptoms: Magic or mystery is arbitrary. "Because plot" answers. Deus ex machina solutions. Reveals feel random rather than inevitable. The impossible has no internal logic.
Key Questions:
- Do the impossible elements follow internal rules?
- Is the reveal inevitable in hindsight?
- Has the impossibility been set up?
- Does the mystery reward attention?
Fix: Establish internal logic for impossibilities. Set up before payoff. The impossible should feel earned, not arbitrary.
---
State T6: Bumbling for Plot
Symptoms: Characters fail stupidly to serve plot. Competent people act incompetent for convenience. Obvious solutions ignored. Intelligence depends on plot needs.
Key Questions:
- Do characters approach problems methodically?
- Are failures from constraints or stupidity?
- Do characters have skills appropriate to their roles?
- Would a competent person make this mistake?
Fix: Failures should come from constraints, not incompetence. Characters should approach problems methodically. Obstacles should be real, not manufactured.
---
State T7: Baroque Complexity
Symptoms: Complexity mistaken for depth. More layers = smarter. Naval-gazing philosophy. Elaborate systems for their own sake. "The more baroque, the smarter it thinks it is."
Key Questions:
- Does complexity serve a function?
- Could this be simpler without losing meaning?
- Is intricacy earning its weight?
- Is depth coming from complexity or from meaning?
Fix: Simplify to essence. Complexity should serve function, not signal intelligence.
---
Kepler-Specific Dimensions
These dimensions are derived from project taste preferences and used for scoring:
| Dimension | Positive Pole | Negative Pole | Weight |
|---|---|---|---|
| Competence | Capable people facing real constraints | Bumbling idiots serving plot | 1.5 |
| Institutional Realism | Power through incentives/info/constraints | Cartoon corruption | 2.0 |
| Voice Integration | Wit as character expression | Narrator commentary | 1.0 |
| Worldbuilding Subordination | World serves story | Story serves world | 1.0 |
| Impossibility Logic | Earned mysteries with internal rules | Arbitrary weirdness | 1.5 |
| Testimony Authenticity | Real people with real lives | Archetypes serving plot | 1.5 |
Scoring Guide
| Score | Meaning |
|---|---|
| 5 | Exemplary—clearly demonstrates positive pole |
| 4 | Strong—mostly positive with minor issues |
| 3 | Neutral—neither positive nor negative |
| 2 | Weak—leans toward negative pole |
| 1 | Poor—clearly demonstrates negative pole |
| 0 | Absent—dimension not present in content |
---
Diagnostic Process
When evaluating creative work:
1. Check for Taste States
Scan the work for T1-T7 patterns. For each detected:
- Identify specific passages that trigger the state
- Assess severity (minor, moderate, severe)
- Note if context provides exception (e.g., unreliable narrator)
2. Score Against Dimensions
For each dimension present in the work:
- Score 0-5 based on alignment with positive/negative poles
- Apply weights to get weighted score
- Note specific evidence for score
3. Generate Report
Output:
- Detected taste states with passages
- Dimension scores with evidence
- Overall taste alignment (weighted average)
- Recommendations for alignment
4. Consider Preference Conflicts
When multiple contributors have preferences:
- Identify where preferences conflict
- Note which preference is being served
- Suggest options that balance competing tastes
---
Integration with Other Skills
From story-sense
When story-sense identifies State 7: Ready for Evaluation, taste-eval provides preference-based evaluation alongside craft evaluation.
State 7 (story-sense)
├── Craft evaluation (sensitivity-check, genre-conventions)
└── Preference evaluation (taste-eval)From revision
Taste-eval adds a Taste Pass after the Prose Pass and before the Polish Pass:
| Pass | Focus |
|---|---|
| 5. Prose | Sentence level—clarity, flow, precision |
| 6. Taste | Preference alignment—dimensions, states |
| 7. Polish | Mechanics—grammar, spelling, formatting |
From drafting
Before drafting, check taste dimensions as pre-flight:
- Which dimensions are most relevant to this scene?
- Are there preferences that should guide voice?
- What taste states should be avoided?
---
Available Tools
taste-check.ts
Pattern-match text for taste state violations.
# Check file for all taste states
deno run --allow-read scripts/taste-check.ts testimony.md
# Check specific states only
deno run --allow-read scripts/taste-check.ts --states T1,T2 scene.md
# Inline text check
deno run --allow-read scripts/taste-check.ts --text "The government was too stupid..."
# Output as JSON
deno run --allow-read scripts/taste-check.ts --json testimony.mdOutput: Flagged passages with state identification and severity.
taste-audit.ts
Score content against taste dimensions.
# Full audit with recommendations
deno run --allow-read scripts/taste-audit.ts testimony.md
# Scores only (no recommendations)
deno run --allow-read scripts/taste-audit.ts --scores-only chapter.md
# Compare to specific preferences file
deno run --allow-read scripts/taste-audit.ts --prefs taste.md testimony.mdOutput: Dimension scores (0-5), weighted average, recommendations.
preference-map.ts
Visualize preference distribution and balance.
# Generate preference map from taste file
deno run --allow-read scripts/preference-map.ts context/foundation/taste.md
# Check if content balances multiple preferences
deno run --allow-read scripts/preference-map.ts --check chapter.md
# Show conflicts between contributors
deno run --allow-read scripts/preference-map.ts --conflicts taste.mdOutput: Preference distribution, conflict points, balance recommendations.
---
Anti-Patterns in Taste Evaluation
The Preference Police
Problem: Using taste as enforcement rather than guidance. Fix: Taste surfaces alignment, not right/wrong. Work can intentionally violate preferences.
The Consensus Seeker
Problem: Trying to satisfy all preferences equally. Fix: Creative friction is the goal. Some preferences will dominate in some scenes.
The Detector Override
Problem: Dismissing all detected patterns as "intentional." Fix: Detection prompts reflection. Even intentional violations should be conscious.
The Score Optimizer
Problem: Maximizing dimension scores instead of serving story. Fix: Scores are diagnostic, not goals. Some stories benefit from low scores on some dimensions.
---
Example Interactions
Example 1: Testimony Draft Review
Writer: "Does this testimony align with project taste?"
Your approach: 1. Run taste-check for T1-T7 patterns 2. Score against Kepler dimensions 3. Flag specific passages that trigger states 4. Recommend adjustments or confirm alignment 5. Note if violations are intentional (character voice, etc.)
Example 2: Voting Options Generation
AI generating options: "I need three options for how the Captain responds to the crisis."
Your approach: 1. Review taste preferences for all contributors 2. Ensure options reflect diverse preferences 3. Flag if options cluster around one preference 4. Suggest options that create productive friction
Example 3: Pre-Flight Check
Writer: "About to draft Testimony 47. Any taste considerations?"
Your approach: 1. Identify relevant dimensions for this testimony 2. Note any preferences that should guide voice 3. Flag taste states to avoid 4. Suggest specific elements that align with taste
---
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 taste-eval session?"
- Suggest:
explorations/taste-eval/or a sensible location
4. Store preference in context/output-config.md or .taste-eval-output.md
Primary Output
For this skill, persist:
- Taste state detections with passages and severity
- Dimension scores with evidence and weights
- Preference conflicts when multiple contributors
- Recommendations for alignment
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| State detections with passages | Clarifying questions |
| Dimension scores with evidence | Discussion of trade-offs |
| Conflict analysis | Writer's preference decisions |
| Alignment recommendations | Real-time feedback |
File Naming
Pattern: {content}-taste-{date}.md Example: testimony-047-taste-2026-01-03.md
---
What You Do NOT Do
- You do not enforce taste as rules—preferences are guidance
- You do not override intentional violations—surface them, don't forbid
- You do not optimize for scores—serve the story
- You do not resolve preference conflicts—surface them for decision
- You do not write for the writer—diagnose and recommend
---
Key Insight
Taste is not about right or wrong. It's about making the implicit explicit. When preferences are visible, decisions become conscious. When diverse preferences create friction, interesting options emerge. The goal is not alignment but awareness.
Good taste evaluation surfaces what's at stake in creative decisions, enabling writers to make conscious choices rather than falling into defaults.
{
"_meta": {
"description": "Project-specific taste dimensions derived from The Kepler Testimonies taste.md",
"source": "context/foundation/taste.md",
"updated": "2026-01-03",
"usage": "Score creative work (0-5) on each dimension; weight multiplies for overall score"
},
"dimensions": [
{
"id": "competence",
"name": "Competence vs. Bumbling",
"description": "Characters solve problems methodically; failures come from constraints, not stupidity",
"positive_pole": "Capable people facing real constraints",
"negative_pole": "Bumbling idiots serving plot convenience",
"weight": 1.5,
"check_questions": [
"Do characters approach problems methodically?",
"Are failures caused by constraints rather than stupidity?",
"Do characters have skills appropriate to their roles?",
"Would a competent person in this situation act similarly?"
],
"positive_indicators": [
"Character uses available resources logically",
"Failures come from genuine constraints",
"Problem-solving shows domain knowledge",
"Characters learn from mistakes"
],
"negative_indicators": [
"Character ignores obvious solutions",
"Failures serve plot convenience",
"Competence varies based on scene needs",
"Idiot ball passed to enable conflict"
]
},
{
"id": "institutional_realism",
"name": "Institutional Realism",
"description": "Power flows through incentives, information, and constraints—not cartoon corruption",
"positive_pole": "Power through incentives/information/constraints",
"negative_pole": "Cartoon corruption and institutional stupidity",
"weight": 2.0,
"check_questions": [
"Do authority figures have constraints they're working within?",
"Is there an internal perspective on institutions?",
"Are failures explained by incentives rather than malice?",
"Do people within systems understand problems they can't fix?"
],
"positive_indicators": [
"Institution has internal logic",
"Characters face incentive structures",
"Information asymmetry creates conflict",
"Reform is hard, not because people are evil"
],
"negative_indicators": [
"All officials are corrupt or stupid",
"Only outsiders see the truth",
"Institutions exist to be obstacles",
"Government/corporation = bad, full stop"
]
},
{
"id": "voice_integration",
"name": "Voice Integration",
"description": "Wit comes from character, not narrator commentary",
"positive_pole": "Wit as character expression",
"negative_pole": "Narrator commentary and meta-humor",
"weight": 1.0,
"check_questions": [
"Does humor come from character or narrator?",
"Is voice consistent with the POV character?",
"Do jokes break the story frame?",
"Is wit grounded in situation or in author cleverness?"
],
"positive_indicators": [
"Wit emerges from character perspective",
"Humor fits the character's voice",
"Jokes arise from situation",
"Voice stays in story world"
],
"negative_indicators": [
"Narrator makes asides to reader",
"Meta-humor breaks frame",
"Author cleverness overrides character",
"Winking at the audience"
]
},
{
"id": "worldbuilding_subordination",
"name": "Worldbuilding Subordination",
"description": "World serves story, not the reverse; details earn their place",
"positive_pole": "World serves story",
"negative_pole": "Story serves world; exposition for its own sake",
"weight": 1.0,
"check_questions": [
"Does each world detail earn its place through action?",
"Would the story survive without this exposition?",
"Is worldbuilding delivered through character experience?",
"Does setting enhance story, or dominate it?"
],
"positive_indicators": [
"World details revealed through action",
"Exposition integrated with character goals",
"Setting creates stakes and constraints",
"Mysteries unfold naturally"
],
"negative_indicators": [
"Story stops for exposition",
"Character exists to explain world",
"Lengthy descriptions of systems",
"Worldbuilding as end in itself"
]
},
{
"id": "impossibility_logic",
"name": "Impossibility Logic",
"description": "The 12 impossible testimonies have internal rules; mystery is earned, not arbitrary",
"positive_pole": "Earned mysteries with internal rules",
"negative_pole": "Arbitrary weirdness for effect",
"weight": 1.5,
"check_questions": [
"Do the impossible elements follow internal rules?",
"Is the reveal inevitable in hindsight?",
"Has the impossibility been properly set up?",
"Does the mystery reward attention?"
],
"positive_indicators": [
"Impossibility has consistent rules",
"Hints planted for later revelation",
"Mystery deepens on re-read",
"Weirdness serves larger purpose"
],
"negative_indicators": [
"Random supernatural elements",
"Because plot said so",
"Deus ex machina resolutions",
"Weirdness for its own sake"
]
},
{
"id": "testimony_authenticity",
"name": "Testimony Authenticity",
"description": "Testimonies come from real people with real lives, not archetypes serving plot",
"positive_pole": "Real people with real lives",
"negative_pole": "Archetypes serving plot function",
"weight": 1.5,
"check_questions": [
"Does this person feel like they have a life beyond the story?",
"Are details specific rather than generic?",
"Does voice feel distinct from other testimonies?",
"Would this person exist without the plot?"
],
"positive_indicators": [
"Specific personal details",
"Voice distinct from others",
"Concerns beyond plot relevance",
"History that predates the story"
],
"negative_indicators": [
"Generic character type",
"Exists only to witness events",
"Interchangeable with other testimonies",
"No life outside story function"
]
}
],
"scoring": {
"scale": {
"5": "Exemplary—clearly demonstrates positive pole",
"4": "Strong—mostly positive with minor issues",
"3": "Neutral—neither positive nor negative",
"2": "Weak—leans toward negative pole",
"1": "Poor—clearly demonstrates negative pole",
"0": "Absent—dimension not present in content"
},
"calculation": {
"method": "weighted_average",
"formula": "sum(score * weight) / sum(weight)",
"note": "Only include dimensions present in content (score > 0)"
},
"interpretation": {
"4.0+": "Strong alignment with project taste",
"3.0-3.9": "Adequate alignment, some areas for improvement",
"2.0-2.9": "Significant taste misalignment—review recommended",
"below_2.0": "Major revision likely needed"
}
}
}
{
"_meta": {
"description": "Pattern libraries for detecting taste state violations",
"usage": "Scan text for trigger patterns; check severity modifiers; consider context exceptions",
"version": "1.0"
},
"states": {
"T1_institutional_cartoon": {
"name": "Institutional Cartoon",
"description": "Simplistic portrayals of institutions as uniformly dumb or evil",
"triggers": [
"too stupid to see",
"too dumb to understand",
"of course the government",
"corrupt officials",
"just another bureaucrat",
"faceless corporation",
"evil corporation",
"typical government incompetence",
"wake up and see",
"what they don't want you to know",
"the system is designed to",
"they're all in on it",
"only the hero understood",
"no one listened",
"the authorities refused to",
"predictable red tape",
"soulless bureaucracy"
],
"severity_modifiers": {
"high": ["always", "never", "all of them", "everyone knows", "obviously"],
"medium": ["just another", "typical", "as usual", "of course"],
"low": ["sometimes", "often", "tends to"]
},
"context_exceptions": [
"Spoken by unreliable narrator with later correction",
"Character's limited perspective explicitly challenged",
"Ironic framing made clear",
"Critique of character's worldview, not author's"
],
"fix_suggestions": [
"Show specific constraints the institution faces",
"Add internal perspective from someone within the system",
"Explain failure through incentives, not stupidity",
"Show competent people trapped by structural issues"
]
},
"T2_cynicism_as_cleverness": {
"name": "Cynicism as Cleverness",
"description": "Snark and nihilism substituting for insight",
"triggers": [
"nothing ever changes",
"that's just how it is",
"what did you expect",
"people never change",
"there's no point in",
"might as well give up",
"it's all a joke",
"everything's meaningless",
"nobody cares about",
"that's adorable",
"oh, sweet summer child",
"you actually believe",
"naive enough to think",
"welcome to the real world"
],
"severity_modifiers": {
"high": ["clearly", "obviously", "everyone with a brain knows"],
"medium": ["apparently", "supposedly", "I guess"],
"low": ["seems like", "might be"]
},
"context_exceptions": [
"Character in depression arc with later growth",
"Cynicism shown as character flaw being addressed",
"Explicitly framed as incorrect worldview",
"Followed by genuine engagement"
],
"fix_suggestions": [
"Ground critique in specific mechanisms",
"Show what's actually at stake",
"Give cynical character something to care about",
"Distinguish earned skepticism from performative nihilism"
]
},
"T3_worldbuilding_dominance": {
"name": "Worldbuilding Dominance",
"description": "Story stops for exposition; setting overwhelms character",
"triggers": [
"let me explain how",
"you see, in our world",
"the history of this place",
"as everyone knows",
"it should be noted that",
"for those unfamiliar with",
"the rules state that",
"according to tradition",
"in the days before",
"the system works as follows"
],
"severity_modifiers": {
"high": ["first, you must understand", "to fully comprehend", "it's important to know"],
"medium": ["basically", "essentially", "in short"],
"low": ["briefly", "simply put"]
},
"structural_patterns": [
"Three or more paragraphs of pure exposition",
"Character asks obvious question to trigger info-dump",
"New character introduced primarily to explain",
"Action pauses for worldbuilding"
],
"context_exceptions": [
"In-world document (testimony format justifies exposition)",
"Character discovery integrating world info",
"Exposition creates immediate stakes",
"World detail triggers character action"
],
"fix_suggestions": [
"Deliver world info through character experience",
"Integrate exposition with action",
"Cut to essential details only",
"Let mystery remain—reader doesn't need everything explained"
]
},
"T4_narrator_commentary": {
"name": "Narrator Commentary",
"description": "Author voice intrudes; wit comes from outside story",
"triggers": [
"dear reader",
"you might be wondering",
"as you can imagine",
"if you're thinking that",
"spoiler alert",
"plot twist",
"but I digress",
"where were we",
"fourth wall",
"as we say in",
"wink wink",
"nudge nudge"
],
"voice_inconsistencies": [
"Vocabulary suddenly shifts to modern slang in period piece",
"Character makes pop culture reference they wouldn't know",
"Narration more clever than POV character would be",
"Knowing asides that break perspective"
],
"context_exceptions": [
"Established omniscient narrator voice",
"Framed as character's journal or testimony",
"Stylistic choice consistent throughout",
"Genre conventions (comedy, parody) permit meta-awareness"
],
"fix_suggestions": [
"Ground wit in character perspective",
"Keep vocabulary consistent with POV",
"Let humor arise from situation",
"If meta-commentary needed, frame it clearly"
]
},
"T5_unearned_impossibility": {
"name": "Unearned Impossibility",
"description": "Magic/mystery is arbitrary; no internal logic",
"triggers": [
"suddenly, inexplicably",
"for some reason",
"without warning",
"out of nowhere",
"it just happened",
"no one knew why",
"mysteriously",
"against all logic",
"defying explanation",
"somehow"
],
"severity_modifiers": {
"high": ["completely impossible", "utterly inexplicable", "made no sense at all"],
"medium": ["strange", "odd", "unusual"],
"low": ["unexpected", "surprising"]
},
"structural_patterns": [
"Resolution depends on previously unestablished element",
"Mystery has no internal rules",
"Supernatural element serves convenience",
"Deus ex machina resolution"
],
"context_exceptions": [
"Mystery meant to remain mysterious (with thematic purpose)",
"Early in story with later explanation",
"Character POV—they don't understand, but rules exist",
"Cosmic horror intentionally beyond comprehension"
],
"fix_suggestions": [
"Establish internal rules for impossibility",
"Plant hints for later revelation",
"Ensure mystery rewards attention on re-read",
"Give even strange things consistent logic"
]
},
"T6_bumbling_for_plot": {
"name": "Bumbling for Plot",
"description": "Characters fail stupidly to serve story convenience",
"triggers": [
"didn't think to",
"forgot to mention",
"if only they had",
"why didn't they just",
"it never occurred to",
"failed to notice",
"overlooked the obvious",
"left it behind",
"didn't bother to"
],
"structural_patterns": [
"Competent character makes obvious mistake",
"Information conveniently withheld",
"Easy solution ignored",
"Character's intelligence varies by scene needs"
],
"severity_modifiers": {
"high": ["stupidly", "inexplicably", "for no good reason"],
"medium": ["carelessly", "hastily", "without thinking"],
"low": ["unfortunately", "regrettably"]
},
"context_exceptions": [
"Character is established as fallible in this way",
"Stress or time pressure explains oversight",
"Information asymmetry is established",
"Failure comes from constraint, not incompetence"
],
"fix_suggestions": [
"Give character real constraints that cause failure",
"Establish character limitations early",
"Create genuine obstacles, not manufactured ones",
"Let competent characters be competent"
]
},
"T7_baroque_complexity": {
"name": "Baroque Complexity",
"description": "Complexity for its own sake; depth confused with intricacy",
"triggers": [
"it's more complicated than that",
"the situation is complex",
"there are layers to this",
"you couldn't possibly understand",
"wheels within wheels",
"deeper meaning",
"hidden significance",
"on multiple levels"
],
"structural_patterns": [
"Subplots that don't resolve or connect",
"Excessive point-of-view switches",
"Philosophical tangents that don't serve story",
"Unexplained symbolism treated as depth"
],
"severity_modifiers": {
"high": ["infinitely complex", "beyond comprehension", "impossibly intricate"],
"medium": ["quite complex", "rather involved", "somewhat convoluted"],
"low": ["a bit complicated", "not straightforward"]
},
"context_exceptions": [
"Complexity serves thematic purpose",
"Intricacy creates meaningful tension",
"Layers are actually connected",
"Earned complexity (mystery genre, etc.)"
],
"fix_suggestions": [
"Ask what each layer accomplishes",
"Simplify to essence without losing meaning",
"Ensure complexity serves story function",
"Cut what's clever but not necessary"
]
}
},
"detection_config": {
"case_sensitive": false,
"require_word_boundaries": true,
"minimum_confidence": 0.6,
"context_window": 200,
"severity_levels": ["low", "medium", "high"]
}
}
#!/usr/bin/env -S deno run --allow-read
/**
* preference-map.ts
*
* Visualize preference distribution and balance across contributors.
*
* Usage:
* deno run --allow-read scripts/preference-map.ts context/foundation/taste.md
* deno run --allow-read scripts/preference-map.ts --conflicts taste.md
* deno run --allow-read scripts/preference-map.ts --check content.md
*/
import { parse } from "https://deno.land/std@0.208.0/flags/mod.ts";
// Types
interface Contributor {
name: string;
preferences: {
loves: string[];
wants_more: string[];
wants_less: string[];
pet_peeves: string[];
non_negotiables: string[];
};
}
interface ConflictPoint {
topic: string;
perspectives: Array<{
contributor: string;
stance: "positive" | "negative" | "neutral";
detail: string;
}>;
severity: "high" | "medium" | "low";
}
interface PreferenceMap {
contributors: Contributor[];
conflicts: ConflictPoint[];
commonGround: string[];
forcedTradeoffs: string[];
}
// Parse a taste.md file to extract contributor preferences
function parseTasteFile(content: string): PreferenceMap {
const contributors: Contributor[] = [];
const forcedTradeoffs: string[] = [];
// Split by contributor sections (### Name)
const sections = content.split(/^### /m).filter(s => s.trim());
for (const section of sections) {
const lines = section.split("\n");
const nameLine = lines[0]?.trim();
// Skip non-contributor sections
if (!nameLine || nameLine.includes("Template") || nameLine.includes("Forced")) {
// Check for forced tradeoffs section
if (section.includes("We do") || section.includes("We don't") || section.includes(", not")) {
const tradeoffMatches = section.match(/[-*]\s*\*\*(.+?)\*\*/g) || [];
for (const match of tradeoffMatches) {
forcedTradeoffs.push(match.replace(/[-*]\s*\*\*/g, "").replace(/\*\*/g, "").trim());
}
}
continue;
}
const contributor: Contributor = {
name: nameLine.replace(/\(.*?\)/g, "").trim(),
preferences: {
loves: [],
wants_more: [],
wants_less: [],
pet_peeves: [],
non_negotiables: [],
},
};
// Extract preferences by section headers
let currentSection = "";
for (const line of lines) {
const lowerLine = line.toLowerCase();
if (lowerLine.includes("love") || lowerLine.includes("works i love")) {
currentSection = "loves";
} else if (lowerLine.includes("want more")) {
currentSection = "wants_more";
} else if (lowerLine.includes("want less")) {
currentSection = "wants_less";
} else if (lowerLine.includes("pet peeve") || lowerLine.includes("biggest pet peeve")) {
currentSection = "pet_peeves";
} else if (lowerLine.includes("for this project") || lowerLine.includes("non-negotiable")) {
currentSection = "non_negotiables";
} else if (line.startsWith("-") && currentSection) {
const item = line.replace(/^-\s*/, "").trim();
if (item && contributor.preferences[currentSection as keyof typeof contributor.preferences]) {
contributor.preferences[currentSection as keyof typeof contributor.preferences].push(item);
}
}
}
if (contributor.preferences.loves.length > 0 ||
contributor.preferences.wants_more.length > 0 ||
contributor.preferences.pet_peeves.length > 0) {
contributors.push(contributor);
}
}
// Analyze for conflicts
const conflicts = analyzeConflicts(contributors);
// Find common ground
const commonGround = findCommonGround(contributors);
return {
contributors,
conflicts,
commonGround,
forcedTradeoffs,
};
}
// Analyze preferences for potential conflicts
function analyzeConflicts(contributors: Contributor[]): ConflictPoint[] {
const conflicts: ConflictPoint[] = [];
// Keywords that might indicate opposing preferences
const oppositionPatterns = [
{ positive: ["grounded", "realistic"], negative: ["fantasy", "magical"] },
{ positive: ["fast-paced", "action"], negative: ["slow", "contemplative"] },
{ positive: ["dark", "gritty"], negative: ["hopeful", "optimistic"] },
{ positive: ["complex", "layered"], negative: ["simple", "streamlined"] },
{ positive: ["humorous", "wit"], negative: ["serious", "dramatic"] },
];
for (const pattern of oppositionPatterns) {
const perspectives: ConflictPoint["perspectives"] = [];
for (const contributor of contributors) {
const allPrefs = [
...contributor.preferences.loves,
...contributor.preferences.wants_more,
].join(" ").toLowerCase();
const allAversions = [
...contributor.preferences.wants_less,
...contributor.preferences.pet_peeves,
].join(" ").toLowerCase();
let stance: "positive" | "negative" | "neutral" = "neutral";
let detail = "";
for (const pos of pattern.positive) {
if (allPrefs.includes(pos)) {
stance = "positive";
detail = `Prefers ${pos}`;
break;
}
if (allAversions.includes(pos)) {
stance = "negative";
detail = `Dislikes ${pos}`;
break;
}
}
if (stance === "neutral") {
for (const neg of pattern.negative) {
if (allPrefs.includes(neg)) {
stance = "positive";
detail = `Prefers ${neg}`;
break;
}
if (allAversions.includes(neg)) {
stance = "negative";
detail = `Dislikes ${neg}`;
break;
}
}
}
if (stance !== "neutral") {
perspectives.push({
contributor: contributor.name,
stance,
detail,
});
}
}
// Only report if there's actual conflict (different stances)
if (perspectives.length >= 2) {
const hasPositive = perspectives.some(p => p.stance === "positive");
const hasNegative = perspectives.some(p => p.stance === "negative");
if (hasPositive && hasNegative) {
conflicts.push({
topic: pattern.positive.join("/"),
perspectives,
severity: "medium",
});
}
}
}
return conflicts;
}
// Find common ground across contributors
function findCommonGround(contributors: Contributor[]): string[] {
if (contributors.length < 2) return [];
const common: string[] = [];
// Look for similar themes in loves and wants_more
const allPositives = contributors.map(c => [
...c.preferences.loves,
...c.preferences.wants_more,
].join(" ").toLowerCase());
// Keywords that appear across multiple contributors
const keywords = [
"character", "grounded", "competent", "realistic", "wit", "voice",
"distinct", "authentic", "earned", "constraints", "institutional",
];
for (const keyword of keywords) {
const count = allPositives.filter(p => p.includes(keyword)).length;
if (count >= Math.ceil(contributors.length * 0.6)) {
common.push(`Shared interest: ${keyword}`);
}
}
return common;
}
// Format preference map for console
function formatConsoleOutput(map: PreferenceMap, showConflicts: boolean): string {
const lines: string[] = [];
lines.push("\n🗺️ Preference Map");
lines.push("═".repeat(50));
// Contributors overview
lines.push(`\nContributors: ${map.contributors.length}`);
for (const contrib of map.contributors) {
lines.push(` • ${contrib.name}`);
if (contrib.preferences.loves.length > 0) {
lines.push(` Loves: ${contrib.preferences.loves.slice(0, 2).join(", ")}`);
}
if (contrib.preferences.pet_peeves.length > 0) {
lines.push(` Pet peeves: ${contrib.preferences.pet_peeves.slice(0, 1).join(", ")}`);
}
}
// Forced tradeoffs
if (map.forcedTradeoffs.length > 0) {
lines.push("\n📋 Forced Tradeoffs:");
lines.push("─".repeat(50));
for (const tradeoff of map.forcedTradeoffs) {
lines.push(` • ${tradeoff}`);
}
}
// Common ground
if (map.commonGround.length > 0) {
lines.push("\n✅ Common Ground:");
lines.push("─".repeat(50));
for (const ground of map.commonGround) {
lines.push(` • ${ground}`);
}
}
// Conflicts
if (showConflicts && map.conflicts.length > 0) {
lines.push("\n⚠️ Potential Conflicts:");
lines.push("─".repeat(50));
for (const conflict of map.conflicts) {
lines.push(` Topic: ${conflict.topic}`);
for (const p of conflict.perspectives) {
const icon = p.stance === "positive" ? "👍" : "👎";
lines.push(` ${icon} ${p.contributor}: ${p.detail}`);
}
lines.push("");
}
} else if (map.conflicts.length === 0) {
lines.push("\n✅ No significant conflicts detected.");
}
lines.push("");
return lines.join("\n");
}
// Format JSON output
function formatJsonOutput(map: PreferenceMap): string {
return JSON.stringify(map, null, 2);
}
// Main
async function main() {
const args = parse(Deno.args, {
boolean: ["json", "help", "conflicts", "check"],
alias: { h: "help", j: "json", c: "conflicts" },
});
if (args.help) {
console.log(`
preference-map.ts - Visualize preference distribution across contributors
Usage:
deno run --allow-read preference-map.ts [options] <taste-file>
Options:
-h, --help Show this help message
-j, --json Output as JSON
-c, --conflicts Show detailed conflict analysis
--check FILE Check if content balances preferences (not yet implemented)
The taste file should be in the project's taste.md format with:
- Individual contributor sections (### Name)
- Loves/Works I Love sections
- Want More/Want Less sections
- Pet peeves
- Forced Tradeoffs section
Examples:
deno run --allow-read preference-map.ts context/foundation/taste.md
deno run --allow-read preference-map.ts --conflicts taste.md
`);
Deno.exit(0);
}
// Get file to parse
if (args._.length === 0) {
console.error("Error: No taste file provided. Use --help for usage.");
Deno.exit(1);
}
const fileName = String(args._[0]);
let content: string;
try {
content = await Deno.readTextFile(fileName);
} catch (error) {
console.error(`Error reading file ${fileName}:`, error.message);
Deno.exit(1);
}
// Parse the taste file
const map = parseTasteFile(content);
// Output
if (args.json) {
console.log(formatJsonOutput(map));
} else {
console.log(formatConsoleOutput(map, args.conflicts));
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* taste-audit.ts
*
* Score content against taste dimensions.
*
* Usage:
* deno run --allow-read scripts/taste-audit.ts testimony.md
* deno run --allow-read scripts/taste-audit.ts --scores-only chapter.md
* deno run --allow-read scripts/taste-audit.ts --prefs taste.md testimony.md
*/
import { parse } from "https://deno.land/std@0.208.0/flags/mod.ts";
// Types
interface Dimension {
id: string;
name: string;
description: string;
positive_pole: string;
negative_pole: string;
weight: number;
check_questions: string[];
positive_indicators: string[];
negative_indicators: string[];
}
interface DimensionsData {
dimensions: Dimension[];
scoring: {
scale: Record<string, string>;
calculation: {
method: string;
formula: string;
};
interpretation: Record<string, string>;
};
}
interface DimensionScore {
id: string;
name: string;
score: number;
weight: number;
weightedScore: number;
positiveMatches: string[];
negativeMatches: string[];
confidence: "high" | "medium" | "low";
}
interface AuditResult {
file: string | null;
overallScore: number;
interpretation: string;
dimensions: DimensionScore[];
recommendations: string[];
}
// Load dimensions from data file
async function loadDimensions(): Promise<DimensionsData> {
const scriptDir = new URL(".", import.meta.url).pathname;
const dimensionsPath = `${scriptDir}../data/kepler-dimensions.json`;
try {
const content = await Deno.readTextFile(dimensionsPath);
return JSON.parse(content);
} catch (error) {
console.error(`Error loading dimensions from ${dimensionsPath}:`, error.message);
Deno.exit(1);
}
}
// Count indicator matches in text
function countIndicatorMatches(text: string, indicators: string[]): string[] {
const matches: string[] = [];
const lowerText = text.toLowerCase();
for (const indicator of indicators) {
// Convert indicator to searchable terms (simplified matching)
const terms = indicator.toLowerCase().split(/\s+/).filter(t => t.length > 4);
// If most terms are found in text, count as match
let found = 0;
for (const term of terms) {
if (lowerText.includes(term)) {
found++;
}
}
if (terms.length > 0 && found >= terms.length * 0.6) {
matches.push(indicator);
}
}
return matches;
}
// Score a dimension based on text analysis
function scoreDimension(text: string, dimension: Dimension): DimensionScore {
const positiveMatches = countIndicatorMatches(text, dimension.positive_indicators);
const negativeMatches = countIndicatorMatches(text, dimension.negative_indicators);
const positiveCount = positiveMatches.length;
const negativeCount = negativeMatches.length;
const totalIndicators = dimension.positive_indicators.length + dimension.negative_indicators.length;
// Calculate base score
let score: number;
const matchRatio = (positiveCount + negativeCount) / totalIndicators;
if (positiveCount === 0 && negativeCount === 0) {
// No indicators found - neutral
score = 0; // Absent
} else if (positiveCount > 0 && negativeCount === 0) {
// Only positive
score = positiveCount >= 2 ? 5 : 4;
} else if (negativeCount > 0 && positiveCount === 0) {
// Only negative
score = negativeCount >= 2 ? 1 : 2;
} else {
// Mixed
const ratio = positiveCount / (positiveCount + negativeCount);
if (ratio >= 0.75) score = 4;
else if (ratio >= 0.5) score = 3;
else if (ratio >= 0.25) score = 2;
else score = 1;
}
// Determine confidence
let confidence: "high" | "medium" | "low";
if (matchRatio >= 0.3) confidence = "high";
else if (matchRatio >= 0.1) confidence = "medium";
else confidence = "low";
return {
id: dimension.id,
name: dimension.name,
score: score,
weight: dimension.weight,
weightedScore: score * dimension.weight,
positiveMatches: positiveMatches,
negativeMatches: negativeMatches,
confidence: confidence,
};
}
// Calculate overall score from dimensions
function calculateOverallScore(scores: DimensionScore[]): number {
const presentScores = scores.filter(s => s.score > 0);
if (presentScores.length === 0) {
return 0;
}
const weightedSum = presentScores.reduce((sum, s) => sum + s.weightedScore, 0);
const totalWeight = presentScores.reduce((sum, s) => sum + s.weight, 0);
return weightedSum / totalWeight;
}
// Get interpretation for score
function getInterpretation(score: number, interpretations: Record<string, string>): string {
if (score >= 4.0) return interpretations["4.0+"] || "Strong alignment";
if (score >= 3.0) return interpretations["3.0-3.9"] || "Adequate alignment";
if (score >= 2.0) return interpretations["2.0-2.9"] || "Significant misalignment";
return interpretations["below_2.0"] || "Major revision needed";
}
// Generate recommendations based on scores
function generateRecommendations(scores: DimensionScore[], dimensions: Dimension[]): string[] {
const recommendations: string[] = [];
for (const score of scores) {
if (score.score > 0 && score.score <= 2) {
const dimension = dimensions.find(d => d.id === score.id);
if (dimension) {
recommendations.push(
`[${dimension.name}] Low score (${score.score}/5): ${dimension.description}`
);
recommendations.push(
` ↳ Aim for: ${dimension.positive_pole}`
);
if (score.negativeMatches.length > 0) {
recommendations.push(
` ↳ Found issues: ${score.negativeMatches.slice(0, 2).join("; ")}`
);
}
}
}
}
if (recommendations.length === 0) {
const lowConfidence = scores.filter(s => s.confidence === "low" && s.score > 0);
if (lowConfidence.length > 0) {
recommendations.push(
"Note: Some dimensions had low confidence scores due to limited indicator matches."
);
}
}
return recommendations;
}
// Format console output
function formatConsoleOutput(result: AuditResult, scoresOnly: boolean): string {
const lines: string[] = [];
if (result.file) {
lines.push(`\n📊 Taste Audit: ${result.file}`);
} else {
lines.push(`\n📊 Taste Audit`);
}
lines.push("═".repeat(50));
// Overall score
const scoreBar = "█".repeat(Math.round(result.overallScore)) + "░".repeat(5 - Math.round(result.overallScore));
lines.push(`\nOverall Score: ${result.overallScore.toFixed(1)}/5.0 [${scoreBar}]`);
lines.push(`Interpretation: ${result.interpretation}`);
lines.push("");
// Dimension scores
lines.push("Dimension Scores:");
lines.push("─".repeat(50));
for (const dim of result.dimensions) {
if (dim.score === 0) {
lines.push(` ${dim.name}: — (not present)`);
continue;
}
const dimBar = "●".repeat(dim.score) + "○".repeat(5 - dim.score);
const confidenceIcon = dim.confidence === "high" ? "✓" : dim.confidence === "medium" ? "~" : "?";
lines.push(` ${dim.name}: ${dim.score}/5 [${dimBar}] ${confidenceIcon}`);
if (!scoresOnly) {
if (dim.positiveMatches.length > 0) {
lines.push(` ✅ ${dim.positiveMatches.slice(0, 2).join(", ")}`);
}
if (dim.negativeMatches.length > 0) {
lines.push(` ⚠️ ${dim.negativeMatches.slice(0, 2).join(", ")}`);
}
}
}
// Recommendations
if (!scoresOnly && result.recommendations.length > 0) {
lines.push("");
lines.push("Recommendations:");
lines.push("─".repeat(50));
for (const rec of result.recommendations) {
lines.push(rec);
}
}
lines.push("");
return lines.join("\n");
}
// Format JSON output
function formatJsonOutput(result: AuditResult): string {
return JSON.stringify(result, null, 2);
}
// Main
async function main() {
const args = parse(Deno.args, {
string: ["prefs"],
boolean: ["json", "help", "scores-only"],
alias: { h: "help", j: "json", p: "prefs", s: "scores-only" },
});
if (args.help) {
console.log(`
taste-audit.ts - Score content against taste dimensions
Usage:
deno run --allow-read taste-audit.ts [options] <file>
Options:
-h, --help Show this help message
-j, --json Output as JSON
-s, --scores-only Show only scores, no details
-p, --prefs FILE Use custom preferences file (not yet implemented)
Dimensions scored:
• Competence - characters solve problems methodically
• Institutional Realism - power through incentives/constraints
• Voice Integration - wit as character expression
• Worldbuilding Subordination - world serves story
• Impossibility Logic - earned mysteries with rules
• Testimony Authenticity - real people with real lives
Scoring:
5 = Exemplary (clearly positive)
4 = Strong (mostly positive)
3 = Neutral
2 = Weak (leans negative)
1 = Poor (clearly negative)
0 = Absent (dimension not present)
Examples:
deno run --allow-read taste-audit.ts testimony.md
deno run --allow-read taste-audit.ts --scores-only chapter.md
`);
Deno.exit(0);
}
const dimensionsData = await loadDimensions();
// Get file to audit
if (args._.length === 0) {
console.error("Error: No file provided. Use --help for usage.");
Deno.exit(1);
}
const fileName = String(args._[0]);
let text: string;
try {
text = await Deno.readTextFile(fileName);
} catch (error) {
console.error(`Error reading file ${fileName}:`, error.message);
Deno.exit(1);
}
// Score each dimension
const scores: DimensionScore[] = [];
for (const dimension of dimensionsData.dimensions) {
scores.push(scoreDimension(text, dimension));
}
// Calculate overall score
const overallScore = calculateOverallScore(scores);
const interpretation = getInterpretation(overallScore, dimensionsData.scoring.interpretation);
const recommendations = generateRecommendations(scores, dimensionsData.dimensions);
const result: AuditResult = {
file: fileName,
overallScore: overallScore,
interpretation: interpretation,
dimensions: scores,
recommendations: recommendations,
};
// Output
if (args.json) {
console.log(formatJsonOutput(result));
} else {
console.log(formatConsoleOutput(result, args["scores-only"]));
}
// Exit with code based on score
if (overallScore < 2.0) {
Deno.exit(1);
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* taste-check.ts
*
* Pattern-match text for taste state violations (T1-T7).
*
* Usage:
* deno run --allow-read scripts/taste-check.ts testimony.md
* deno run --allow-read scripts/taste-check.ts --states T1,T2 scene.md
* deno run --allow-read scripts/taste-check.ts --text "The government was too stupid..."
* deno run --allow-read scripts/taste-check.ts --json testimony.md
*/
import { parse } from "https://deno.land/std@0.208.0/flags/mod.ts";
// Types
interface TriggerMatch {
state: string;
stateName: string;
trigger: string;
passage: string;
line: number;
severity: "low" | "medium" | "high";
context: string;
}
interface StatePattern {
name: string;
description: string;
triggers: string[];
severity_modifiers: {
high: string[];
medium: string[];
low: string[];
};
context_exceptions: string[];
fix_suggestions: string[];
}
interface PatternsData {
states: Record<string, StatePattern>;
detection_config: {
case_sensitive: boolean;
require_word_boundaries: boolean;
context_window: number;
};
}
interface CheckResult {
file: string | null;
totalMatches: number;
matches: TriggerMatch[];
summary: Record<string, number>;
}
// Load patterns from data file
async function loadPatterns(): Promise<PatternsData> {
const scriptDir = new URL(".", import.meta.url).pathname;
const patternsPath = `${scriptDir}../data/taste-patterns.json`;
try {
const content = await Deno.readTextFile(patternsPath);
return JSON.parse(content);
} catch (error) {
console.error(`Error loading patterns from ${patternsPath}:`, error.message);
Deno.exit(1);
}
}
// Get context around a match
function getContext(text: string, matchIndex: number, windowSize: number): string {
const start = Math.max(0, matchIndex - windowSize);
const end = Math.min(text.length, matchIndex + windowSize);
let context = text.slice(start, end);
if (start > 0) context = "..." + context;
if (end < text.length) context = context + "...";
return context.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
}
// Get line number for a position in text
function getLineNumber(text: string, position: number): number {
const textBeforeMatch = text.slice(0, position);
return (textBeforeMatch.match(/\n/g) || []).length + 1;
}
// Determine severity based on surrounding text
function determineSeverity(
context: string,
modifiers: { high: string[]; medium: string[]; low: string[] }
): "low" | "medium" | "high" {
const lowerContext = context.toLowerCase();
for (const mod of modifiers.high) {
if (lowerContext.includes(mod.toLowerCase())) {
return "high";
}
}
for (const mod of modifiers.medium) {
if (lowerContext.includes(mod.toLowerCase())) {
return "medium";
}
}
// Default to medium if no modifiers found
return "medium";
}
// Check text against all patterns
function checkText(
text: string,
patterns: PatternsData,
selectedStates: string[] | null
): TriggerMatch[] {
const matches: TriggerMatch[] = [];
const lowerText = text.toLowerCase();
for (const [stateId, stateData] of Object.entries(patterns.states)) {
// Skip if state not in selected list
if (selectedStates && !selectedStates.includes(stateId.toUpperCase())) {
continue;
}
for (const trigger of stateData.triggers) {
const lowerTrigger = trigger.toLowerCase();
let searchIndex = 0;
while (true) {
const matchIndex = lowerText.indexOf(lowerTrigger, searchIndex);
if (matchIndex === -1) break;
// Check word boundaries if required
if (patterns.detection_config.require_word_boundaries) {
const charBefore = lowerText[matchIndex - 1] || " ";
const charAfter = lowerText[matchIndex + lowerTrigger.length] || " ";
const isWordBoundaryBefore = /\W/.test(charBefore);
const isWordBoundaryAfter = /\W/.test(charAfter);
if (!isWordBoundaryBefore || !isWordBoundaryAfter) {
searchIndex = matchIndex + 1;
continue;
}
}
const context = getContext(
text,
matchIndex,
patterns.detection_config.context_window
);
const lineNumber = getLineNumber(text, matchIndex);
const passage = text.slice(matchIndex, matchIndex + trigger.length);
const severity = determineSeverity(context, stateData.severity_modifiers);
matches.push({
state: stateId.toUpperCase().replace("_", "").slice(0, 2),
stateName: stateData.name,
trigger: trigger,
passage: passage,
line: lineNumber,
severity: severity,
context: context,
});
searchIndex = matchIndex + 1;
}
}
}
// Sort by line number
return matches.sort((a, b) => a.line - b.line);
}
// Format output for console
function formatConsoleOutput(result: CheckResult): string {
const lines: string[] = [];
if (result.file) {
lines.push(`\n📋 Taste Check: ${result.file}`);
} else {
lines.push(`\n📋 Taste Check: (inline text)`);
}
lines.push("═".repeat(50));
if (result.totalMatches === 0) {
lines.push("✅ No taste pattern violations detected.");
return lines.join("\n");
}
lines.push(`\n⚠️ Found ${result.totalMatches} potential taste pattern(s):\n`);
for (const match of result.matches) {
const severityIcon =
match.severity === "high" ? "🔴" :
match.severity === "medium" ? "🟡" : "🟢";
lines.push(`${severityIcon} [${match.state}] ${match.stateName}`);
lines.push(` Line ${match.line}: "${match.trigger}"`);
lines.push(` Context: ${match.context}`);
lines.push("");
}
// Summary
lines.push("─".repeat(50));
lines.push("Summary by state:");
for (const [state, count] of Object.entries(result.summary)) {
lines.push(` ${state}: ${count}`);
}
return lines.join("\n");
}
// Format output as JSON
function formatJsonOutput(result: CheckResult): string {
return JSON.stringify(result, null, 2);
}
// Main
async function main() {
const args = parse(Deno.args, {
string: ["states", "text"],
boolean: ["json", "help"],
alias: { h: "help", j: "json", s: "states", t: "text" },
});
if (args.help) {
console.log(`
taste-check.ts - Detect taste state violations in text
Usage:
deno run --allow-read taste-check.ts [options] [file]
Options:
-h, --help Show this help message
-j, --json Output as JSON
-s, --states STATES Comma-separated list of states to check (e.g., T1,T2)
-t, --text TEXT Check inline text instead of file
States:
T1 Institutional Cartoon - simplistic institutional portrayals
T2 Cynicism as Cleverness - snark without substance
T3 Worldbuilding Dominance - exposition overwhelms story
T4 Narrator Commentary - author voice intrudes
T5 Unearned Impossibility - arbitrary magic/mystery
T6 Bumbling for Plot - characters fail stupidly
T7 Baroque Complexity - complexity for its own sake
Examples:
deno run --allow-read taste-check.ts testimony.md
deno run --allow-read taste-check.ts --states T1,T2 scene.md
deno run --allow-read taste-check.ts --text "The government was too stupid..."
`);
Deno.exit(0);
}
const patterns = await loadPatterns();
// Get text to check
let text: string;
let fileName: string | null = null;
if (args.text) {
text = args.text;
} else if (args._.length > 0) {
fileName = String(args._[0]);
try {
text = await Deno.readTextFile(fileName);
} catch (error) {
console.error(`Error reading file ${fileName}:`, error.message);
Deno.exit(1);
}
} else {
console.error("Error: No text or file provided. Use --help for usage.");
Deno.exit(1);
}
// Parse selected states
let selectedStates: string[] | null = null;
if (args.states) {
selectedStates = args.states.split(",").map((s: string) => s.trim().toUpperCase());
}
// Run check
const matches = checkText(text, patterns, selectedStates);
// Create summary
const summary: Record<string, number> = {};
for (const match of matches) {
const key = `${match.state}: ${match.stateName}`;
summary[key] = (summary[key] || 0) + 1;
}
const result: CheckResult = {
file: fileName,
totalMatches: matches.length,
matches: matches,
summary: summary,
};
// Output
if (args.json) {
console.log(formatJsonOutput(result));
} else {
console.log(formatConsoleOutput(result));
}
// Exit with code 1 if high severity matches found
const hasHighSeverity = matches.some(m => m.severity === "high");
if (hasHighSeverity) {
Deno.exit(1);
}
}
main();