@@ -1,9 +1,16 @@
import { detectPassiveVoice } from './passiveVoice'
export interface PolishMatch {
from : number
to : number
text : string
}
export interface PolishDimension {
label : string
score : number // 0– 100
score : number
issueCount : number
matches : PolishMatch [ ]
}
export interface PolishScore {
@@ -24,29 +31,62 @@ export interface PolishScore {
}
}
// Score a rate (issues per 1000 words): 0 issues = 100, penalises linearly up to `cap`
function rateScore ( issueCount : number , wordCount : number , cap : number ) : number {
if ( wordCount === 0 ) return 100
const rate = ( issueCount / wordCount ) * 1000
return Math . max ( 0 , Math . round ( 100 - ( rate / cap ) * 100 ) )
}
function stripMarkdown ( text : string ) : string {
return text
. replace ( /^#{1,6}\s+/gm , '' )
. replace ( /\*\*?|__?/g , '' )
. replace ( /\[([^\]]+)\]\([^)]+\)/g , '$1' )
. replace ( /`[^`]+`/g , '' )
. replace ( /^>\s+/gm , '' )
}
function countWordsIn ( text : string ) : number {
const t = text . trim ( )
return t === '' ? 0 : t.split ( /\s+/ ) . length
}
function splitSentences ( text : string ) : string [ ] {
return text . split ( /(?<=[.!?])\s+/ ) . map ( s = > s . trim ( ) ) . filter ( s = > s . length > 0 )
// Returns {text, from, to} for each sentence, positions in the raw text
function splitSentencesWithPos ( text : string ) : Array < { text : string ; from : number ; to : number } > {
const result : Array < { text : string ; from : number ; to : number } > = [ ]
const re = /[.!?]+\s+/g
let last = 0
let m : RegExpExecArray | null
while ( ( m = re . exec ( text ) ) !== null ) {
const to = m . index + m [ 0 ] . length
const s = text . slice ( last , to ) . trim ( )
if ( s . length > 0 ) result . push ( { text : s , from : last , to } )
last = to
}
if ( last < text . length ) {
const s = text . slice ( last ) . trim ( )
if ( s . length > 0 ) result . push ( { text : s , from : last , to : text.length } )
}
return result
}
function splitParasWithPos ( text : string ) : Array < { text : string ; from : number ; to : number } > {
const result : Array < { text : string ; from : number ; to : number } > = [ ]
const re = /\n{2,}/g
let last = 0
let m : RegExpExecArray | null
while ( ( m = re . exec ( text ) ) !== null ) {
const s = text . slice ( last , m . index ) . trim ( )
if ( s . length > 0 ) result . push ( { text : s , from : last , to : m.index } )
last = m . index + m [ 0 ] . length
}
if ( last < text . length ) {
const s = text . slice ( last ) . trim ( )
if ( s . length > 0 ) result . push ( { text : s , from : last , to : text.length } )
}
return result
}
function regexMatches ( text : string , pattern : RegExp ) : PolishMatch [ ] {
const re = new RegExp ( pattern . source , pattern . flags . includes ( 'g' ) ? pattern.flags : pattern.flags + 'g' )
re . lastIndex = 0
const result : PolishMatch [ ] = [ ]
let m : RegExpExecArray | null
while ( ( m = re . exec ( text ) ) !== null ) {
result . push ( { from : m . index , to : m.index + m [ 0 ] . length , text : m [ 0 ] } )
}
return result
}
// ── Weasel words ──────────────────────────────────────────────────────────────
@@ -54,128 +94,153 @@ const WEASEL_PATTERN = /\b(very|quite|rather|fairly|really|basically|actually|li
// ── Adverb density ────────────────────────────────────────────────────────────
const ADVERB_PATTERN = /\b\w+ly\b/gi
const ADVERB_EXCEPTIONS = new Set ( [ 'only' , 'early' , 'daily' , 'likely' , 'lonely' , 'lovely' , 'elderly' , 'friendly' , 'lively' , 'deadly' , 'holy' , 'ugly' , 'silly' , 'hilly' , 'belly' , 'bully' , 'rally' , 'ally' , 'jelly' , 'fully' , 'ully' ])
const ADVERB_EXCEPTIONS = new Set ( [ 'only' , 'early' , 'daily' , 'likely' , 'lonely' , 'lovely' , 'elderly' , 'friendly' , 'lively' , 'deadly' , 'holy' , 'ugly' , 'silly' , 'hilly' , 'belly' , 'bully' , 'rally' , 'ally' , 'jelly' , 'fully' ] )
function adverbDensityScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const m atches = ( text . match ( ADVERB_PATTERN ) ? ? [ ] ) . filter ( w = > ! ADVERB_EXCEPTIONS . has ( w . toLowerCase ( ) ) )
return { score : rateScore ( matches . length , wordCount , 20 ) , issueCount : matches.length }
function adverbMatches ( text : string ) : PolishMatch [ ] {
return regexM atches( text , ADVERB_PATTERN ) . filter ( m = > ! ADVERB_EXCEPTIONS . has ( m . text . toLowerCase ( ) ) )
}
// ── Filter words ──────────────────────────────────────────────────────────────
const FILTER_PATTERN = /\b(she saw|he saw|she heard|he heard|she felt|he felt|she noticed|he noticed|she realized|he realized|she thought|he thought|she wondered|he wondered|she knew|he knew|she watched|he watched|she could see|he could see|she could hear|he could hear|she remembered|he remembered|she decided|he decided)\b/gi
function filterWordScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const matches = ( text . match ( FILTER_PATTERN ) ? ? [ ] ) . length
return { score : rateScore ( matches , wordCount , 8 ) , issueCount : matches }
}
// ── Show / tell proxies ───────────────────────────────────────────────────────
const EMOTION_ADJECTIVES = 'angry|sad|happy|afraid|scared|nervous|anxious|excited|lonely|confused|surprised|disappointed|embarrassed|ashamed|jealous|tired|bored|furious|terrified|delighted|miserable|guilty|proud|content|relieved|frustrated|hopeful|desperate|bitter|disgusted|horrified|depressed|irritated|overwhelmed|resentful|heartbroken|ecstatic|joyful|melancholy|sorrowful|gloomy|cheerful|peaceful|calm|worried|tense|uneasy|weary'
const SHOW_TELL_PATTERN = new RegExp ( ` \\ b(was|were|is|are) \\ s+(very \\ s+)?( ${ EMOTION_ADJECTIVES } ) \\ b ` , 'gi' )
function showTellScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const matches = ( text . match ( SHOW_TELL_PATTERN ) ? ? [ ] ) . length
return { score : rateScore ( matches , wordCount , 10 ) , issueCount : matches }
}
// ── Sentence rhythm ───────────────────────────────────────────────────────────
function sentenceRhythmScore ( text : string ) : { score : number ; issueCount : number } {
const sentences = splitSentences ( text )
if ( sentences . length < 3 ) return { score : 100 , issueCount : 0 }
function sentenceRhythmAnalysis ( text : string ) : { score : number ; matches : PolishMatch [ ] } {
const sentences = splitSentencesWithPos ( text )
if ( sentences . length < 3 ) return { score : 100 , matches : [ ] }
const lengths = sentences . map ( s = > s . split ( /\s+/ ) . length )
const lengths = sentences . map ( s = > s . text . split( /\s+/ ) . length )
const mean = lengths . reduce ( ( a , b ) = > a + b , 0 ) / lengths . length
const variance = lengths . reduce ( ( a , b ) = > a + ( b - mean ) * * 2 , 0 ) / lengths . length
const cv = mean > 0 ? Math . sqrt ( variance ) / mean : 0
let runsCount = 0
const matches : PolishMatch [ ] = [ ]
let runStart = 0
let runLen = 1
for ( let i = 1 ; i < lengths . length ; i ++ ) {
if ( Math . abs ( lengths [ i ] - lengths [ i - 1 ] ) <= 2 ) {
runLen ++
if ( runLen === 3 ) runsCount ++
if ( runLen === 3 ) runStart = i - 2
if ( runLen >= 3 ) {
const s = sentences [ runStart ]
const e = sentences [ i ]
const existing = matches . find ( m = > m . from === s . from )
if ( existing ) {
existing . to = e . to
existing . text = text . slice ( s . from , e . to )
} else {
matches . push ( { from : s . from , to : e.to , text : text.slice ( s . from , e . to ) } )
}
}
} else {
runLen = 1
}
}
const score = Math . min ( 100 , Math . round ( ( cv / 0.6 ) * 100 ) )
return { score , issueCount : runsCount }
return { score , matches }
}
// ── Paragraph rhythm ──────────────────────────────────────────────────────────
function paragraphRhythmScore ( text : string ) : { score : number ; issueCount : number } {
const paragraph s = text . split ( /\n{2,}/ ) . map ( p = > p . trim ( ) ) . filter ( p = > p . length > 0 )
if ( paragraph s . length < 3 ) return { score : 100 , issueCount : 0 }
function paragraphRhythmAnalysis ( text : string ) : { score : number ; matches : PolishMatch [ ] } {
const paras = splitParasWithPos ( text )
if ( paras . length < 3 ) return { score : 100 , matches : [ ] }
const lengths = paragraph s . map ( p = > countWordsIn ( p ) )
const lengths = paras . map ( p = > countWordsIn ( p . text ))
const mean = lengths . reduce ( ( a , b ) = > a + b , 0 ) / lengths . length
const variance = lengths . reduce ( ( a , b ) = > a + ( b - mean ) * * 2 , 0 ) / lengths . length
const cv = mean > 0 ? Math . sqrt ( variance ) / mean : 0
// Count wall-of-text paragraphs (> 150 words) and tiny runs (≤ 5 words each)
const walls = lengths . filter ( l = > l > 150 ) . length
const matches : PolishMatch [ ] = paras
. filter ( ( _ , i ) = > lengths [ i ] > 150 )
. map ( p = > ( { from : p . from , to : p.to , text : p.text } ) )
const score = Math . min ( 100 , Math . round ( ( cv / 0.8 ) * 100 ) )
return { score , issueCount : wall s }
return { score , matche s }
}
// ── Repeated sentence starters ────────────────────────────────────────────────
function repeatedStarterScore ( text : string ) : { score : number ; issueCount : number } {
const sentences = splitSentences ( text )
if ( sentences . length < 4 ) return { score : 100 , issueCount : 0 }
const PRONOUNS = new Set ( [ 'he' , 'she' , 'i' , 'they' , 'it' , 'we' , 'you' ] )
const starters = sentences . map ( s = > s . split ( /\s+/ ) [ 0 ] ? . toLowerCase ( ) . replace ( /[^a-z]/g , '' ) ? ? '' )
function repeatedStarterAnalysi s( text : string ) : { score : number ; matches : PolishMatch [ ] } {
const sentences = splitSentencesWithPos ( text )
if ( sentences . length < 4 ) return { score : 100 , matches : [ ] }
let runs = 0
const starters = sentences . map ( s = > s . text . split ( /\s+/ ) [ 0 ] ? . toLowerCase ( ) . replace ( /[^a-z]/g , '' ) ? ? '' )
const matches : PolishMatch [ ] = [ ]
let runLen = 1
let runStart = 0
for ( let i = 1 ; i < starters . length ; i ++ ) {
if ( starters [ i ] === starters [ i - 1 ] && starters [ i ] !== '' ) {
runLen ++
if ( runLen === 3 ) runs ++
if ( runLen === 3 ) runStart = i - 2
if ( runLen >= 3 ) {
const s = sentences [ runStart ]
const e = sentences [ i ]
const existing = matches . find ( m = > m . from === s . from )
if ( existing ) { existing . to = e . to ; existing . text = text . slice ( s . from , e . to ) }
else matches . push ( { from : s . from , to : e.to , text : text.slice ( s . from , e . to ) } )
}
} else {
runLen = 1
}
}
// Also penalise pronoun-heavy openings overall
const PRONOUNS = new Set ( [ 'he' , 'she' , 'i' , 'they' , 'it' , 'we' , 'you' ] )
const pronounStarts = starters . filter ( w = > PRONOUNS . has ( w ) ) . length
const pronounRatio = pronounStarts / starters . length
const runPenalty = Math . min ( 60 , runs * 15 )
const runPenalty = Math . min ( 60 , matches . length * 15 )
const pronounPenalty = Math . max ( 0 , Math . round ( ( pronounRatio - 0.4 ) / 0.4 * 40 ) )
const score = Math . max ( 0 , 100 - runPenalty - pronounPenalty )
return { score , issueCount : run s }
return { score , matche s }
}
// ── Word variety ──────────────────────────────────────────────────────────────
const SKIP_WORDS = new Set ( [ 'the' , 'a' , 'an' , 'and' , 'or' , 'but' , 'in' , 'on' , 'at' , 'to' , 'for' , 'of' , 'with' , 'is' , 'was' , 'are' , 'were' , 'it' , 'he' , 'she' , 'they' , 'i' , 'you' , 'we' , 'be' , 'been' , 'being' , 'that' , 'this' , 'those' , 'these' , 'have' , 'has' , 'had' , 'do' , 'did' , 'does' , 'not' , 'so' , 'as' , 'if' , 'by' , 'from' , 'into' , 'than' , 'then' , 'when' , 'where' , 'who' , 'which' , 'what' , 'his' , 'her' , 'their' , 'its' , 'my' , 'your' , 'our' , 'up' , 'out' , 'about' , 'after' , 'before' , 'all' , 'some' , 'one' , 'no' , 'more' , 'also' ] )
function wordVarietyScore ( text : string ) : { score : number ; issueCount : number } {
const words = text . toLowerCase ( ) . match ( /\b[a-z']+\b/g ) ? ? [ ]
if ( word s. length < 20 ) return { score : 100 , issueCount : 0 }
function wordVarietyAnalysis ( text : string ) : { score : number ; matches : PolishMatch [ ] } {
const allWordMatches = [ . . . text . matchAll ( /\b[a-z']+\b/gi ) ]
if ( allWordMatche s. length < 20 ) return { score : 100 , matches : [ ] }
const contentWords = word s. filter ( w = > ! SKIP_WORDS . has ( w ) )
if ( contentWords . length < 20 ) return { score : 100 , issueCount : 0 }
const contentWordMatche s = allWordMatche s. filter ( m = > ! SKIP_WORDS . has ( m [ 0 ] . toLowerCase ( ) ) )
if ( contentWordMatche s . length < 20 ) return { score : 100 , matches : [ ] }
const WINDOW = 100
const words = contentWordMatches . map ( m = > m [ 0 ] . toLowerCase ( ) )
const ttrs : number [ ] = [ ]
for ( let i = 0 ; i + WINDOW <= contentW ords. length ; i += Math . floor ( WINDOW / 2 ) ) {
const win = contentW ords. slice ( i , i + WINDOW )
for ( let i = 0 ; i + WINDOW <= w ords. length ; i += Math . floor ( WINDOW / 2 ) ) {
const win = w ords. slice ( i , i + WINDOW )
ttrs . push ( new Set ( win ) . size / win . length )
}
const avgTTR = ttrs . reduce ( ( a , b ) = > a + b , 0 ) / ttrs. length
const avgTTR = ttrs . length > 0 ? ttrs . reduce( ( a , b ) = > a + b , 0 ) / ttrs. length : 1
const score = Math . min ( 100 , Math . max ( 0 , Math . round ( ( ( avgTTR - 0.4 ) / 0.3 ) * 100 ) ) )
let repeatedCount = 0
for ( let i = 0 ; i + 50 <= contentWords . length ; i += 25 ) {
cons t win = contentWords . slice ( i , i + 50 )
const counts = new Map < string , number > ( )
for ( const w of win ) counts . set ( w , ( counts . get ( w ) ? ? 0 ) + 1 )
for ( const [ , count ] of counts ) if ( count >= 3 ) repeatedCount ++
// Find words that appear 3+ times within any 50-word window; highlight duplicates
const flagged = new Set < number > ( ) // indices into contentWordMatches
for ( le t i = 0 ; i + 50 <= words . length ; i += 2 5) {
const win = words . slice ( i , i + 50 )
const counts = new Map < string , number [ ] > ( )
for ( let j = 0 ; j < win . length ; j ++ ) {
const w = win [ j ]
if ( ! counts . has ( w ) ) counts . set ( w , [ ] )
counts . get ( w ) ! . push ( i + j )
}
for ( const [ , indices ] of counts ) {
if ( indices . length >= 3 ) indices . forEach ( idx = > flagged . add ( idx ) )
}
}
const score = Math. min ( 100 , Math . max ( 0 , Math . round ( ( ( avgTTR - 0.4 ) / 0.3 ) * 100 ) ) )
return { sc ore , issueCount : repeatedCount }
const matches : Polish Matc h[ ] = [ . . . flagged ]
. sort ( ( a , b ) = > a - b )
. map ( idx = > {
const m = contentWordMatches [ idx ]
return { from : m . index ! , to : m.index ! + m [ 0 ] . length , text : m [ 0 ] }
} )
return { score , matches }
}
// ── Clichés ───────────────────────────────────────────────────────────────────
@@ -187,88 +252,79 @@ const CLICHES = [
'face drained' , 'colour drained' , 'blood froze' , 'heart stopped' , 'chest tightened' ,
'lump in her throat' , 'lump in his throat' , 'pit of her stomach' , 'pit of his stomach' ,
'at the end of the day' , 'all hell broke loose' , 'couldn\'t believe her eyes' ,
'couldn\'t believe his eyes' , 'needle in a haystack ' , 'tip of the iceber g' ,
'dead of night' , 'ray of hope' , 'last but not least' , 'speak of the devil ' ,
'cold as ice' , 'sharp as a knife' , ' black as night' , 'white as snow' , 'red as blood' ,
'silence was deafening' , 'deafening silence' , 'elephant in the room' ,
'couldn\'t believe his eyes' , 'dead of night ' , 'ray of hope' , 'silence was deafenin g' ,
'deafening silence' , 'elephant in the room' , 'cold as ice' , 'sharp as a knife ' ,
'black as night' , 'white as snow' , 'red as blood' ,
]
const CLICHE_PATTERN = new RegExp ( CLICHES . map ( c = > c . replace ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) ) . join ( '|' ) , 'gi' )
function clicheScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const matches = ( text . match ( CLICHE_PATTERN ) ? ? [ ] ) . length
return { score : rateScore ( matches , wordCount , 6 ) , issueCount : matches }
}
// ── Overused filler words ─────────────────────────────────────────────────────
const OVERUSED_PATTERN = /\b(just|suddenly|started to|began to|that|somehow| somehow|anyway|whatever| stuff|things|got|get )\b/gi
function overusedWordScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const matches = ( text . match ( OVERUSED_PATTERN ) ? ? [ ] ) . length
return { score : rateScore ( matches , wordCount , 25 ) , issueCount : matches }
}
const OVERUSED_PATTERN = /\b(just|suddenly|started to|began to|somehow|anyway|stuff|things)\b/gi
// ── Dialogue tags ─────────────────────────────────────────────────────────────
const SAID_BOOKISMS = /\b(hissed|snapped|exclaimed|barked|spat|snarled|growled|shrieked|whined|croaked|breathed|murmured)\b/gi
const ADV_TAG = /\b(said|asked|replied|whispered)\s+\w+ly\b/gi
function dialogueTagScore ( text : string , wordCount : number ) : { score : number ; issueCount : number } {
const total = ( text . match ( SAID_BOOKISMS ) ? ? [ ] ) . length + ( text . match ( ADV_TAG ) ? ? [ ] ) . length
return { score : rateScore ( total , wordCount , 8 ) , issueCount : total }
}
// ── Main export ───────────────────────────────────────────────────────────────
export function computePolishScore ( rawText : string ) : PolishScore {
const text = stripMarkdow n( rawText )
const wordCount = countWordsIn ( text )
const wordCount = countWordsI n( rawText )
const skip = wordCount <= 10
const dim = ( label : string , score : number , issueCount : number ) : PolishDimension = > ( { label , score , issueCount } )
function dim ( label : string , score : number , matches : PolishMatch [ ] ) : PolishDimension {
return { label , score , issueCount : matches.length , matches }
}
const passiveIssue s = skip ? 0 : detectPassiveVoice( t ext) . length
const weaselIssues = skip ? 0 : ( text . match ( WEASEL_PATTERN ) ? ? [ ] ) . length
const passiveAnnotation s = skip ? [ ] : detectPassiveVoice ( rawT ext)
const passiveMatches : PolishMatch [ ] = passiveAnnotations
. filter ( a = > a . from !== undefined && a . to !== undefined )
. map ( a = > ( { from : a . from ! , to : a.to ! , text : a.matchedText ? ? '' } ) )
const adverb = skip ? { score : 100 , issueCount : 0 } : adverbDensityScore ( t ext, wordCount )
const filter = skip ? { score : 100 , issueCount : 0 } : filterWordScore ( text , wordCoun t)
const showTell = skip ? { score : 100 , issueCount : 0 } : showTellScore ( t ext, wordCount )
const sRhythm = skip ? { score : 100 , issueCount : 0 } : sentenceRhythmScore ( text )
const p Rhythm = skip ? { score : 100 , issueCount : 0 } : paragraphRhythmScore ( t ext)
const starters = skip ? { score : 100 , issueCount : 0 } : repeatedStarterScore ( t ext)
const variety = skip ? { score : 100 , issueCount : 0 } : wordVarietyScore ( t ext)
const cliche = skip ? { score : 100 , issueCount : 0 } : clicheScore ( text , wordCoun t)
const overused = skip ? { score : 100 , issueCount : 0 } : overusedWordScore ( t ext, wordCount )
const dialogue = skip ? { score : 100 , issueCount : 0 } : dialogueTagScore ( t ext, wordCount )
const weaselMatches = skip ? [ ] : regexMatches ( rawT ext, WEASEL_PATTERN )
const adverbMatches_ = skip ? [ ] : adverbMatches ( rawTex t)
const filterMatches = skip ? [ ] : regexMatches ( rawT ext, FILTER_PATTERN )
const showTellMatches = skip ? [ ] : regexMatches ( rawText , SHOW_TELL_PATTERN )
const s Rhythm = skip ? { score : 100 , matches : [ ] } : sentenceRhythmAnalysis ( rawT ext)
const pRhythm = skip ? { score : 100 , matches : [ ] } : paragraphRhythmAnalysis ( rawT ext)
const starters = skip ? { score : 100 , matches : [ ] } : repeatedStarterAnalysis ( rawT ext)
const variety = skip ? { score : 100 , matches : [ ] } : wordVarietyAnalysis ( rawTex t)
const clicheMatches = skip ? [ ] : regexMatches ( rawT ext, CLICHE_PATTERN )
const overusedMatches = skip ? [ ] : regexMatches ( rawT ext, OVERUSED_PATTERN )
const dialogueMatches = skip ? [ ] : [
. . . regexMatches ( rawText , SAID_BOOKISMS ) ,
. . . regexMatches ( rawText , ADV_TAG ) ,
]
const passiveScore = rateScore ( passiveMatches . length , wordCount , 12 )
const weaselScore = rateScore ( weaselMatches . length , wordCount , 10 )
const adverbScore = rateScore ( adverbMatches_ . length , wordCount , 20 )
const filterScore = rateScore ( filterMatches . length , wordCount , 8 )
const showTellScore = rateScore ( showTellMatches . length , wordCount , 10 )
const clicheScore = rateScore ( clicheMatches . length , wordCount , 6 )
const overusedScore = rateScore ( overusedMatches . length , wordCount , 25 )
const dialogueScore = rateScore ( dialogueMatches . length , wordCount , 8 )
const scores = [
rat eScore( passiveIssues , wordCount , 12 ) ,
rateScore ( weaselIssues , wordCount , 10 ) ,
adverb . s core,
filter . score ,
showTell . score ,
sRhythm . score ,
pRhythm . score ,
starters . score ,
variety . score ,
cliche . score ,
overused . score ,
dialogue . score ,
passiv eScore, weaselScore , adverbScore , filterScore , showTellScore ,
sRhythm . score , pRhythm . score , starters . score , variety . score ,
clicheScore , overusedScore , dialogueS core,
]
const overall = Math . round ( scores . reduce ( ( a , b ) = > a + b , 0 ) / scores . length )
return {
overall ,
dimensions : {
passiveVoice : dim ( 'Passive voice' , rateScore ( passiveIssues , wordCount , 12 ) , passiveIssu es ) ,
weaselWords : dim ( 'Weasel words' , rateScore ( weaselIssues , wordCount , 10 ) , weaselIssu es ) ,
adverbDensity : dim ( 'Adverb density' , adverb. s core , adverb. issueCount ) ,
filterWords : dim ( 'Filter words' , filter. s core , filter. issueCount ) ,
showTell : dim ( 'Show / tell' , showTell. s core , showTell. issueCount ) ,
sentenceRhythm : dim ( 'Sentence rhythm' , sRhythm . score , sRhythm . issueCount ) ,
paragraphRhythm : dim ( 'Paragraph rhythm' , pRhythm . score , pRhythm . issueCount ) ,
repeatedStarters : dim ( 'Sentence starters' , starters . score , starters . issueCount ) ,
wordVariety : dim ( 'Word variety' , variety . score , variety . issueCount ) ,
cliches : dim ( 'Clichés' , cliche. s core , cliche. issueCount ) ,
overusedWords : dim ( 'Filler words' , overused. s core , overused. issueCount ) ,
dialogueTags : dim ( 'Dialogue tags' , dialogue. s core , dialogue. issueCount ) ,
passiveVoice : dim ( 'Passive voice' , passiveScore , passiveMatch es ) ,
weaselWords : dim ( 'Weasel words' , weaselScore , weaselMatch es ) ,
adverbDensity : dim ( 'Adverb density' , adverbS core , adverbMatches_ ) ,
filterWords : dim ( 'Filter words' , filterS core , filterMatches ) ,
showTell : dim ( 'Show / tell' , showTellS core , showTellMatches ) ,
sentenceRhythm : dim ( 'Sentence rhythm' , sRhythm . score , sRhythm . matches ) ,
paragraphRhythm : dim ( 'Paragraph rhythm' , pRhythm . score , pRhythm . matches ) ,
repeatedStarters : dim ( 'Sentence starters' , starters . score , starters . matches ) ,
wordVariety : dim ( 'Word variety' , variety . score , variety . matches ) ,
cliches : dim ( 'Clichés' , clicheS core , clicheMatches ) ,
overusedWords : dim ( 'Filler words' , overusedS core , overusedMatches ) ,
dialogueTags : dim ( 'Dialogue tags' , dialogueS core , dialogueMatches ) ,
}
}
}