opengrammar-server 2.0.644277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.npm.md +95 -0
  2. package/bin/opengrammar-server.js +111 -0
  3. package/dist/server.js +48639 -0
  4. package/package.json +80 -0
  5. package/server-node.ts +159 -0
  6. package/server.ts +15 -0
  7. package/src/analyzer.ts +542 -0
  8. package/src/dictionary.ts +1973 -0
  9. package/src/index.ts +978 -0
  10. package/src/nlp/nlp-engine.ts +17 -0
  11. package/src/nlp/tone-analyzer.ts +269 -0
  12. package/src/rephraser.ts +146 -0
  13. package/src/rules/categories/academic-writing.ts +182 -0
  14. package/src/rules/categories/adjectives-adverbs.ts +152 -0
  15. package/src/rules/categories/articles.ts +160 -0
  16. package/src/rules/categories/business-writing.ts +250 -0
  17. package/src/rules/categories/capitalization.ts +79 -0
  18. package/src/rules/categories/clarity.ts +117 -0
  19. package/src/rules/categories/common-errors.ts +601 -0
  20. package/src/rules/categories/confused-words.ts +219 -0
  21. package/src/rules/categories/conjunctions.ts +176 -0
  22. package/src/rules/categories/dangling-modifiers.ts +123 -0
  23. package/src/rules/categories/formality.ts +274 -0
  24. package/src/rules/categories/formatting-idioms.ts +323 -0
  25. package/src/rules/categories/gerund-infinitive.ts +274 -0
  26. package/src/rules/categories/grammar-advanced.ts +294 -0
  27. package/src/rules/categories/grammar.ts +286 -0
  28. package/src/rules/categories/inclusive-language.ts +280 -0
  29. package/src/rules/categories/nouns-pronouns.ts +233 -0
  30. package/src/rules/categories/prepositions-extended.ts +217 -0
  31. package/src/rules/categories/prepositions.ts +159 -0
  32. package/src/rules/categories/punctuation.ts +347 -0
  33. package/src/rules/categories/quantity-agreement.ts +200 -0
  34. package/src/rules/categories/readability.ts +293 -0
  35. package/src/rules/categories/sentence-structure.ts +100 -0
  36. package/src/rules/categories/spelling-advanced.ts +164 -0
  37. package/src/rules/categories/spelling.ts +119 -0
  38. package/src/rules/categories/style-tone.ts +511 -0
  39. package/src/rules/categories/style.ts +78 -0
  40. package/src/rules/categories/subject-verb-agreement.ts +201 -0
  41. package/src/rules/categories/tone-rules.ts +206 -0
  42. package/src/rules/categories/verb-tense.ts +582 -0
  43. package/src/rules/context-filter.ts +446 -0
  44. package/src/rules/index.ts +96 -0
  45. package/src/rules/ruleset-part1-cj-pu-sp.json +657 -0
  46. package/src/rules/ruleset-part1-np-ad-aa-pr.json +831 -0
  47. package/src/rules/ruleset-part1-ss-vt.json +907 -0
  48. package/src/rules/ruleset-part2-cw-st-nf.json +318 -0
  49. package/src/rules/ruleset-part3-aw-bw-il-rd.json +161 -0
  50. package/src/rules/types.ts +79 -0
  51. package/src/shared-types.ts +152 -0
  52. package/src/spellchecker.ts +418 -0
  53. package/tsconfig.json +25 -0
@@ -0,0 +1,17 @@
1
+ import nlp from 'compromise';
2
+
3
+ /**
4
+ * Parses the text into an NLP document to analyze parts of speech, syntax, and sentence structure.
5
+ */
6
+ export function parseNLP(text: string) {
7
+ return nlp(text);
8
+ }
9
+
10
+ /**
11
+ * Helper to find specific parts of speech patterns in the parsed text.
12
+ * Uses compromise's match syntax (e.g., '#Verb #Adjective')
13
+ */
14
+ export function findNLPPattern(text: string, pattern: string) {
15
+ const doc = parseNLP(text);
16
+ return doc.match(pattern);
17
+ }
@@ -0,0 +1,269 @@
1
+ import type { WritingContext } from '../rules/context-filter.js';
2
+
3
+ /**
4
+ * ════════════════════════════════════════════════════════
5
+ * Tone Analyzer — ToneAnalyzer
6
+ * Detects the emotional/professional tone of text.
7
+ * Rule-based Tier 1 (free, instant, no API).
8
+ * ════════════════════════════════════════════════════════
9
+ */
10
+
11
+ export interface ToneSignal {
12
+ type: 'hedging' | 'passive_aggression' | 'apology_filler' | 'negativity' | 'formality_mismatch' | 'confidence_killer' | 'aggressive' | 'positive';
13
+ phrase: string;
14
+ offset: number;
15
+ suggestion: string;
16
+ severity: 'info' | 'warning' | 'error';
17
+ }
18
+
19
+ export interface ToneResult {
20
+ dominant: 'assertive' | 'uncertain' | 'formal' | 'informal' | 'negative' | 'aggressive' | 'friendly' | 'neutral';
21
+ score: number; // 0–100: 100 = very confident/clear
22
+ signals: ToneSignal[];
23
+ tips: string[];
24
+ }
25
+
26
+ // ─── Hedging words/phrases ───
27
+ const HEDGING_WORDS = [
28
+ 'maybe', 'perhaps', 'possibly', 'probably', 'apparently',
29
+ 'i think', 'i guess', 'i suppose', 'i believe', 'i feel like',
30
+ 'sort of', 'kind of', 'somewhat', 'rather', 'fairly',
31
+ 'might', 'could', 'may', 'seem', 'seems', 'seemed', 'appears',
32
+ 'it seems', 'it appears', 'it looks like',
33
+ 'if i understand correctly', 'correct me if i\'m wrong',
34
+ 'roughly', 'approximately', 'more or less', 'in a way', 'as it were',
35
+ ];
36
+
37
+ // ─── Passive-aggressive phrases ───
38
+ const PASSIVE_AGGRESSION_PATTERNS = [
39
+ /\bas (I|we) (said|mentioned|noted|indicated|pointed out|explained|stated)\b/i,
40
+ /\bper my (last|previous|earlier|recent|prior)\b/i,
41
+ /\bas per\b/i,
42
+ /\bgoing forward\b/i,
43
+ /\bnot sure if you (saw|noticed|read|received)\b/i,
44
+ /\bjust (a (gentle|friendly|quick|small) )?(reminder|heads?[ -]?up|note|fyi)\b/i,
45
+ /\bfriendly reminder\b/i,
46
+ /\bhoping you (can|could|will|would|might)\b/i,
47
+ /\bplease (do|kindly) advise\b/i,
48
+ ];
49
+
50
+ // ─── Apology fillers (weakening phrases) ───
51
+ const APOLOGY_FILLER_PATTERNS = [
52
+ /\bjust (wanted|checking|following|wondering|hoping|reaching)\b/i,
53
+ /\bsorry (to (bother|bug|trouble|disturb)|for (the (inconvenience|trouble|delay|confusion)))\b/i,
54
+ /\bif that'?s? (okay|alright|fine|good|acceptable|convenient)\b/i,
55
+ /\bif (you) (don'?t|do) mind\b/i,
56
+ /\bdoes that make sense\b/i,
57
+ /\bif that makes sense\b/i,
58
+ /\bam I making sense\b/i,
59
+ /\bhope this (helps|is helpful|is okay|is fine|finds you well)\b/i,
60
+ /\bjust (a|an) (idea|thought|suggestion|question)\b/i,
61
+ /\bfeel free to\b/i,
62
+ /\bwhenever you get a chance\b/i,
63
+ /\bno rush (but|however|though|,)\b/i,
64
+ ];
65
+
66
+ // ─── Confidence killers ───
67
+ const CONFIDENCE_KILLER_PATTERNS = [
68
+ /\bhopefully\b/i,
69
+ /\bthis (probably|might|may|could) (not )?(work|help|be|make)\b/i,
70
+ /\bi (might be|could be|may be) wrong\b/i,
71
+ /\bnot sure if\b/i,
72
+ /\bnot 100% sure\b/i,
73
+ /\bi'?m no expert\b/i,
74
+ /\btake it (with a grain of salt|or leave it)\b/i,
75
+ /\bdon'?t quote me\b/i,
76
+ /\byou (may|might|could|can) disagree\b/i,
77
+ ];
78
+
79
+ // ─── Informal/slang (flagged in formal contexts) ───
80
+ const INFORMAL_PATTERNS = [
81
+ /\bgonna\b/i, /\bwanna\b/i, /\bgotta\b/i, /\bkinda\b/i, /\bsorta\b/i,
82
+ /\byeah\b/i, /\bnope\b/i, /\byep\b/i, /\bbtw\b/i, /\bfyi\b/i,
83
+ /\blol\b/i, /\bomg\b/i, /\bwtf\b/i, /\bidk\b/i, /\bimo\b/i,
84
+ /\bidk\b/i, /\bimo\b/i, /\btbh\b/i, /\bngl\b/i, /\biykyk\b/i,
85
+ /\bCHERRS?\b/i, /\bcheers\b/i,
86
+ ];
87
+
88
+ // ─── Aggressive phrases ───
89
+ const AGGRESSIVE_PATTERNS = [
90
+ /\byou (always|never|constantly|consistently|repeatedly) (fail|forget|ignore|miss|avoid|dismiss|neglect)\b/i,
91
+ /\bthis is (unacceptable|ridiculous|absurd|outrageous|disgusting)\b/i,
92
+ /\bI demand\b/i,
93
+ /\byou (must|have to|need to) (immediately|now|right now|at once)\b/i,
94
+ /\bwhy (haven'?t|hasn'?t|didn'?t|don'?t|won'?t) you\b/i,
95
+ /\bI'?m (not happy|very (unhappy|upset|disappointed|frustrated|angry))\b/i,
96
+ ];
97
+
98
+ function findInText(text: string, pattern: RegExp): { phrase: string; offset: number } | null {
99
+ const match = text.match(pattern);
100
+ if (!match || match.index === undefined) return null;
101
+ return { phrase: match[0], offset: match.index };
102
+ }
103
+
104
+ function countMatches(text: string, patterns: RegExp[]): number {
105
+ return patterns.filter((p) => p.test(text)).length;
106
+ }
107
+
108
+ function checkHedging(lowerText: string, signals: ToneSignal[], tips: string[]): number {
109
+ const hedgeCount = HEDGING_WORDS.filter((w) => lowerText.includes(w)).length;
110
+ if (hedgeCount >= 2) {
111
+ const firstHedge = HEDGING_WORDS.find((w) => lowerText.includes(w)) || '';
112
+ const offset = lowerText.indexOf(firstHedge);
113
+ signals.push({
114
+ type: 'hedging',
115
+ phrase: firstHedge,
116
+ offset,
117
+ suggestion: `Replace hedging language with direct statements. Instead of "${firstHedge}", use assertive phrasing.`,
118
+ severity: hedgeCount >= 4 ? 'warning' : 'info',
119
+ });
120
+ tips.push(`Your text contains ${hedgeCount} hedging expressions. Try replacing "I think/maybe/perhaps" with direct statements for a more confident tone.`);
121
+ }
122
+ return hedgeCount;
123
+ }
124
+
125
+ function checkPatterns(
126
+ text: string,
127
+ patterns: RegExp[],
128
+ type: ToneSignal['type'],
129
+ severity: ToneSignal['severity'],
130
+ suggestionFn: (phrase: string) => string,
131
+ tipMsg?: string
132
+ ): number {
133
+ let count = 0;
134
+ let firstFound: { phrase: string; offset: number } | null = null;
135
+ for (const pattern of patterns) {
136
+ if (pattern.test(text)) {
137
+ count++;
138
+ if (!firstFound) {
139
+ firstFound = findInText(text, pattern);
140
+ }
141
+ }
142
+ }
143
+ return count;
144
+ }
145
+
146
+ function processSignals(
147
+ text: string,
148
+ patterns: RegExp[],
149
+ type: ToneSignal['type'],
150
+ severity: ToneSignal['severity'],
151
+ suggestionFn: (phrase: string) => string,
152
+ signals: ToneSignal[],
153
+ tips: string[],
154
+ tipMsg?: string
155
+ ): number {
156
+ const count = countMatches(text, patterns);
157
+ if (count >= 1) {
158
+ const found = patterns.map((p) => findInText(text, p)).find(Boolean);
159
+ if (found) {
160
+ signals.push({
161
+ type,
162
+ phrase: found.phrase,
163
+ offset: found.offset,
164
+ suggestion: suggestionFn(found.phrase),
165
+ severity,
166
+ });
167
+ if (tipMsg) tips.push(tipMsg);
168
+ }
169
+ }
170
+ return count;
171
+ }
172
+
173
+ /**
174
+ * Analyze the tone of text based on rule signals.
175
+ * Returns a ToneResult with detected signals and recommendations.
176
+ */
177
+ export function analyzeTone(text: string, context?: WritingContext): ToneResult {
178
+ const lowerText = text.toLowerCase();
179
+ const signals: ToneSignal[] = [];
180
+ const tips: string[] = [];
181
+
182
+ const hedgeCount = checkHedging(lowerText, signals, tips);
183
+
184
+ processSignals(
185
+ text, PASSIVE_AGGRESSION_PATTERNS, 'passive_aggression', 'warning',
186
+ (phrase) => `"${phrase}" can sound passive-aggressive. Consider a more direct alternative.`,
187
+ signals, tips,
188
+ 'Avoid phrases that may sound passive-aggressive (e.g., "as I mentioned", "per my last email"). Be direct instead.'
189
+ );
190
+
191
+ processSignals(
192
+ text, APOLOGY_FILLER_PATTERNS, 'apology_filler', 'info',
193
+ (phrase) => `Remove "${phrase}" — it weakens your message unnecessarily.`,
194
+ signals, tips,
195
+ 'Apology fillers like "just wanted to" or "if that\'s okay" undermine your confidence. Remove them for a stronger message.'
196
+ );
197
+
198
+ processSignals(
199
+ text, CONFIDENCE_KILLER_PATTERNS, 'confidence_killer', 'info',
200
+ (phrase) => `"${phrase}" undermines your authority. State your point directly.`,
201
+ signals, tips
202
+ );
203
+
204
+ const negatives = (text.match(/\b(can'?t|won'?t|never|not|don'?t|doesn'?t|didn'?t|couldn'?t|wouldn'?t|shouldn'?t|haven'?t|hasn'?t|hadn'?t|no\s+one|nobody|nothing|nowhere|neither|nor)\b/gi) || []).length;
205
+ if (negatives >= 4) {
206
+ signals.push({
207
+ type: 'negativity',
208
+ phrase: `${negatives} negative expressions`,
209
+ offset: 0,
210
+ suggestion: 'Consider reframing some negative statements as positive alternatives.',
211
+ severity: negatives >= 7 ? 'warning' : 'info',
212
+ });
213
+ tips.push(`Your text has ${negatives} negative expressions. Reframing some as positive creates a more constructive tone.`);
214
+ }
215
+
216
+ if (context === 'email' || context === 'document' || !context) {
217
+ processSignals(
218
+ text, INFORMAL_PATTERNS, 'formality_mismatch', 'warning',
219
+ (phrase) => `"${phrase}" is too informal for formal writing. Use more formal language.`,
220
+ signals, tips,
221
+ 'Informal words or abbreviations detected. Consider using formal language in this context.'
222
+ );
223
+ }
224
+
225
+ const aggressiveCount = processSignals(
226
+ text, AGGRESSIVE_PATTERNS, 'aggressive', 'error',
227
+ (phrase) => `"${phrase}" sounds aggressive. Try a more constructive phrasing.`,
228
+ signals, tips,
229
+ 'Your text may sound aggressive. Focus on the issue, not the person, and use collaborative language.'
230
+ );
231
+
232
+ const dominant = determineDominantTone(signals, hedgeCount, negatives, aggressiveCount);
233
+ const score = computeScore(signals, hedgeCount, negatives, aggressiveCount);
234
+
235
+ return { dominant, score, signals, tips };
236
+ }
237
+
238
+ function determineDominantTone(
239
+ signals: ToneSignal[],
240
+ hedgeCount: number,
241
+ negatives: number,
242
+ aggressiveCount: number,
243
+ ): ToneResult['dominant'] {
244
+ if (aggressiveCount >= 2) return 'aggressive';
245
+ if (signals.some((s) => s.type === 'aggressive')) return 'aggressive';
246
+ if (negatives >= 7) return 'negative';
247
+ if (hedgeCount >= 4) return 'uncertain';
248
+ if (signals.some((s) => s.type === 'formality_mismatch')) return 'informal';
249
+ if (signals.some((s) => s.type === 'passive_aggression')) return 'aggressive';
250
+ if (hedgeCount >= 2 || signals.some((s) => s.type === 'confidence_killer')) return 'uncertain';
251
+ if (signals.length === 0) return 'assertive';
252
+ return 'neutral';
253
+ }
254
+
255
+ function computeScore(
256
+ signals: ToneSignal[],
257
+ hedgeCount: number,
258
+ negatives: number,
259
+ aggressiveCount: number,
260
+ ): number {
261
+ let score = 100;
262
+ score -= hedgeCount * 8; // each hedge word -8 pts
263
+ score -= negatives * 3; // each negative -3 pts
264
+ score -= aggressiveCount * 15; // aggressive is bad -15 pts
265
+ score -= signals.filter((s) => s.severity === 'warning').length * 10;
266
+ score -= signals.filter((s) => s.severity === 'error').length * 20;
267
+ score -= signals.filter((s) => s.severity === 'info').length * 5;
268
+ return Math.max(0, Math.min(100, score));
269
+ }
@@ -0,0 +1,146 @@
1
+ import { Groq } from 'groq-sdk';
2
+ import OpenAI from 'openai';
3
+ import type { LLMProvider } from './shared-types.js';
4
+
5
+ export type RephraseGoal = 'clarity' | 'formal' | 'concise' | 'friendly';
6
+
7
+ export interface RephraseSuggestion {
8
+ text: string;
9
+ label: string;
10
+ }
11
+
12
+ export interface RephraseResult {
13
+ suggestions: RephraseSuggestion[];
14
+ explanation: string;
15
+ bestMatch: number;
16
+ }
17
+
18
+ const GOAL_DESCRIPTIONS: Record<RephraseGoal, { adjective: string; label: string }> = {
19
+ clarity: { adjective: 'clearer and easier to understand', label: '🎯 Clearer' },
20
+ formal: { adjective: 'more formal and professional', label: '👔 Formal' },
21
+ concise: { adjective: 'shorter and more concise (remove filler words)', label: '✂️ Concise' },
22
+ friendly: { adjective: 'warmer, friendlier, and more approachable', label: '😊 Friendly' },
23
+ };
24
+
25
+ const SYSTEM_PROMPT = `You are an expert writing assistant. Rewrite the given sentence in exactly 3 different ways.
26
+
27
+ Rules:
28
+ 1. Keep the core meaning intact
29
+ 2. Apply the requested goal (clarity/formal/concise/friendly)
30
+ 3. Each rewrite must be meaningfully different from the others
31
+ 4. Keep the same approximate length unless goal is "concise"
32
+ 5. Return ONLY a JSON object — no preamble, no markdown
33
+
34
+ Format:
35
+ {
36
+ "suggestions": [
37
+ { "text": "First rewrite", "label": "Option 1" },
38
+ { "text": "Second rewrite", "label": "Option 2" },
39
+ { "text": "Third rewrite", "label": "Option 3" }
40
+ ],
41
+ "explanation": "One sentence explaining what was improved",
42
+ "bestMatch": 0
43
+ }`;
44
+
45
+ export class Rephraser {
46
+ /**
47
+ * Generate 3 rephrase suggestions for a given sentence.
48
+ * Supports Groq (free), OpenAI, Ollama, and OpenRouter.
49
+ */
50
+ static async rephrase(
51
+ sentence: string,
52
+ goal: RephraseGoal = 'clarity',
53
+ apiKey: string,
54
+ provider: LLMProvider = 'groq',
55
+ model?: string,
56
+ baseUrl?: string,
57
+ ): Promise<RephraseResult> {
58
+ const goalDesc = GOAL_DESCRIPTIONS[goal];
59
+ const userPrompt = `Rewrite this sentence in 3 ways that are ${goalDesc.adjective}:\n\n"${sentence}"`;
60
+
61
+ try {
62
+ if (provider === 'groq') {
63
+ return await Rephraser.rephraseWithGroq(userPrompt, apiKey, model || 'llama3-8b-8192');
64
+ } else {
65
+ return await Rephraser.rephraseWithOpenAI(userPrompt, apiKey, model || 'gpt-3.5-turbo', provider, baseUrl);
66
+ }
67
+ } catch (err) {
68
+ console.error(`[Rephraser] ${provider} error:`, err);
69
+ // Return empty gracefully
70
+ return {
71
+ suggestions: [],
72
+ explanation: 'Rephrase failed. Please check your API key and try again.',
73
+ bestMatch: 0,
74
+ };
75
+ }
76
+ }
77
+
78
+ private static async rephraseWithGroq(
79
+ userPrompt: string,
80
+ apiKey: string,
81
+ model: string,
82
+ ): Promise<RephraseResult> {
83
+ const groq = new Groq({ apiKey });
84
+ const res = await groq.chat.completions.create({
85
+ model,
86
+ messages: [
87
+ { role: 'system', content: SYSTEM_PROMPT },
88
+ { role: 'user', content: userPrompt },
89
+ ],
90
+ response_format: { type: 'json_object' },
91
+ temperature: 0.7,
92
+ max_tokens: 512,
93
+ });
94
+ const raw = res.choices[0]?.message?.content || '{}';
95
+ return Rephraser.parseResult(raw);
96
+ }
97
+
98
+ private static async rephraseWithOpenAI(
99
+ userPrompt: string,
100
+ apiKey: string,
101
+ model: string,
102
+ provider: LLMProvider,
103
+ baseUrl?: string,
104
+ ): Promise<RephraseResult> {
105
+ const providerUrls: Record<string, string> = {
106
+ openai: 'https://api.openai.com/v1',
107
+ openrouter: 'https://openrouter.ai/api/v1',
108
+ together: 'https://api.together.xyz/v1',
109
+ ollama: 'http://localhost:11434/v1',
110
+ custom: baseUrl || '',
111
+ };
112
+ const openai = new OpenAI({
113
+ apiKey: apiKey || 'ollama',
114
+ baseURL: baseUrl || providerUrls[provider as string] || providerUrls.openai,
115
+ });
116
+ const res = await openai.chat.completions.create({
117
+ model,
118
+ messages: [
119
+ { role: 'system', content: SYSTEM_PROMPT },
120
+ { role: 'user', content: userPrompt },
121
+ ],
122
+ response_format: { type: 'json_object' },
123
+ temperature: 0.7,
124
+ max_tokens: 512,
125
+ });
126
+ const raw = res.choices[0]?.message?.content || '{}';
127
+ return Rephraser.parseResult(raw);
128
+ }
129
+
130
+ private static parseResult(raw: string): RephraseResult {
131
+ try {
132
+ const cleaned = raw.replace(/^```json\s*/, '').replace(/\s*```$/, '');
133
+ const parsed = JSON.parse(cleaned);
134
+ return {
135
+ suggestions: (parsed.suggestions || []).map((s: any) => ({
136
+ text: s.text || s,
137
+ label: s.label || 'Option',
138
+ })),
139
+ explanation: parsed.explanation || '',
140
+ bestMatch: typeof parsed.bestMatch === 'number' ? parsed.bestMatch : 0,
141
+ };
142
+ } catch {
143
+ return { suggestions: [], explanation: 'Could not parse response.', bestMatch: 0 };
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,182 @@
1
+ import type { Issue } from '../../shared-types.js';
2
+ import { createRegexRule, type Rule } from '../types.js';
3
+
4
+ /**
5
+ * ═══════════════════════════════════════════════════
6
+ * Academic & Scholarly Writing (AW)
7
+ * Weasel words, vague quantifiers, formality issues
8
+ * ═══════════════════════════════════════════════════
9
+ */
10
+ export const academicWritingRules: Rule[] = [
11
+ // ═══ First Person in Academic (AW_001) ═══
12
+ createRegexRule({
13
+ id: 'AW_I_think',
14
+ category: 'style',
15
+ pattern: /\bI\s+think\s+that\b/i,
16
+ suggestion: 'Evidence suggests that',
17
+ reason: 'Avoid "I think" in academic writing. Use evidence-based language instead.',
18
+ }),
19
+ createRegexRule({
20
+ id: 'AW_I_believe',
21
+ category: 'style',
22
+ pattern: /\bI\s+believe\s+that\b/i,
23
+ suggestion: 'It can be argued that',
24
+ reason: 'Avoid "I believe" in academic writing. Use objective phrasing.',
25
+ }),
26
+ createRegexRule({
27
+ id: 'AW_I_feel',
28
+ category: 'style',
29
+ pattern: /\bI\s+feel\s+that\b/i,
30
+ suggestion: 'The evidence indicates that',
31
+ reason: 'Avoid "I feel" in academic writing. Support claims with evidence.',
32
+ }),
33
+ createRegexRule({
34
+ id: 'AW_in_my_opinion',
35
+ category: 'style',
36
+ pattern: /\bin\s+my\s+opinion\b/i,
37
+ suggestion: 'arguably',
38
+ reason: '"In my opinion" is subjective. Use "arguably" or cite evidence.',
39
+ }),
40
+ createRegexRule({
41
+ id: 'AW_I_personally',
42
+ category: 'style',
43
+ pattern: /\bI\s+personally\b/i,
44
+ suggestion: '(remove or rephrase)',
45
+ reason: '"I personally" is redundant and too subjective for academic writing.',
46
+ }),
47
+ createRegexRule({
48
+ id: 'AW_obviously',
49
+ category: 'style',
50
+ pattern:
51
+ /\b(obviously|clearly|of\s+course|needless\s+to\s+say|it\s+is\s+obvious\s+that|everyone\s+knows)\b/i,
52
+ suggestion: '(remove)',
53
+ reason:
54
+ "If something is truly obvious, it doesn't need to be stated. These phrases can alienate readers who don't find it obvious.",
55
+ }),
56
+
57
+ // ═══ Weasel Words (AW_004) ═══
58
+ createRegexRule({
59
+ id: 'AW_some_people',
60
+ category: 'style',
61
+ pattern: /\bsome\s+people\s+(say|believe|think|argue|claim|suggest|feel)\b/i,
62
+ suggestion: '[Cite specific source]',
63
+ reason: '"Some people say" is vague. Cite specific sources or studies.',
64
+ }),
65
+ createRegexRule({
66
+ id: 'AW_studies_show',
67
+ category: 'style',
68
+ pattern:
69
+ /\bstudies\s+(show|suggest|indicate|have\s+shown|have\s+found|demonstrate|reveal|confirm)\b/i,
70
+ suggestion: '[Author (Year)] found that',
71
+ reason: '"Studies show" is vague. Cite the specific studies (Author, Year).',
72
+ }),
73
+ createRegexRule({
74
+ id: 'AW_experts_say',
75
+ category: 'style',
76
+ pattern:
77
+ /\b(experts|researchers|scientists|scholars|critics|analysts)\s+(say|believe|argue|claim|suggest|agree|think|note|point\s+out)\b/i,
78
+ suggestion: '[Named experts] argue that',
79
+ reason: 'Name the specific experts and cite their work.',
80
+ }),
81
+ createRegexRule({
82
+ id: 'AW_it_is_believed',
83
+ category: 'style',
84
+ pattern:
85
+ /\bit\s+is\s+(widely\s+)?(believed|known|accepted|understood|recognized|acknowledged|thought|assumed|considered)\b/i,
86
+ suggestion: '[Citation needed]',
87
+ reason: 'Who believes/knows this? Provide a citation for the claim.',
88
+ }),
89
+ createRegexRule({
90
+ id: 'AW_many_argue',
91
+ category: 'style',
92
+ pattern:
93
+ /\b(many|several|numerous|various|certain)\s+(people|scholars|researchers|experts|critics|authors|studies)\s+(have\s+)?(argued|suggested|claimed|shown|demonstrated|noted|found|stated|asserted)\b/i,
94
+ suggestion: '[Specific citation needed]',
95
+ reason: 'Quantify or cite specific sources instead of vague attribution.',
96
+ }),
97
+ createRegexRule({
98
+ id: 'AW_recently',
99
+ category: 'style',
100
+ pattern: /\brecently\b/i,
101
+ suggestion: 'In [specific year/period]',
102
+ reason: '"Recently" is vague in academic writing. Specify the time period.',
103
+ }),
104
+
105
+ // ═══ Informal Transitions (AW_006) ═══
106
+ createRegexRule({
107
+ id: 'AW_plus_start',
108
+ category: 'style',
109
+ pattern: /(?:^|[.!?]\s+)Plus\s+/g,
110
+ suggestion: 'Additionally, ',
111
+ reason:
112
+ '"Plus" at the start of a sentence is informal. Use "Additionally", "Moreover", or "Furthermore".',
113
+ }),
114
+ createRegexRule({
115
+ id: 'AW_also_start',
116
+ category: 'style',
117
+ pattern: /(?:^|[.!?]\s+)Also\s*,/g,
118
+ suggestion: 'Furthermore,',
119
+ reason:
120
+ '"Also" starting a sentence is informal. Use "Furthermore", "Moreover", or "In addition".',
121
+ }),
122
+ createRegexRule({
123
+ id: 'AW_so_start',
124
+ category: 'style',
125
+ pattern: /(?:^|[.!?]\s+)So\s+/g,
126
+ suggestion: 'Therefore, ',
127
+ reason: '"So" starting a sentence is informal. Use "Therefore", "Consequently", or "Thus".',
128
+ }),
129
+ createRegexRule({
130
+ id: 'AW_anyway_start',
131
+ category: 'style',
132
+ pattern: /(?:^|[.!?]\s+)Anyway\s*,/g,
133
+ suggestion: 'Nevertheless,',
134
+ reason: '"Anyway" is informal. Use "Nevertheless", "Regardless", or "In any case".',
135
+ }),
136
+ createRegexRule({
137
+ id: 'AW_well_start',
138
+ category: 'style',
139
+ pattern: /(?:^|[.!?]\s+)Well\s*,/g,
140
+ suggestion: '',
141
+ reason: '"Well" as a sentence starter is conversational filler. Remove it in academic writing.',
142
+ }),
143
+ createRegexRule({
144
+ id: 'AW_basically',
145
+ category: 'style',
146
+ pattern: /\bbasically\b/i,
147
+ suggestion: '(remove)',
148
+ reason: '"Basically" is filler in academic writing. State the point directly.',
149
+ }),
150
+ createRegexRule({
151
+ id: 'AW_actually',
152
+ category: 'style',
153
+ pattern: /\bactually\b/i,
154
+ suggestion: '(remove or rephrase)',
155
+ reason: '"Actually" is often filler. Remove it unless correcting a misconception.',
156
+ }),
157
+
158
+ // ═══ Hedging Excess (AW — related to ST_003) ═══
159
+ createRegexRule({
160
+ id: 'AW_sort_of',
161
+ category: 'style',
162
+ pattern:
163
+ /\bsort\s+of\s+(a|an|the|like|similar|different|important|interesting|difficult|easy|good|bad|big|small)\b/i,
164
+ suggestion: (m) => `somewhat ${m[1]}`,
165
+ reason: '"Sort of" is vague and informal. Use "somewhat" or be more precise.',
166
+ }),
167
+ createRegexRule({
168
+ id: 'AW_kind_of',
169
+ category: 'style',
170
+ pattern:
171
+ /\bkind\s+of\s+(a|an|the|like|similar|different|important|interesting|difficult|easy|good|bad|big|small)\b/i,
172
+ suggestion: (m) => `somewhat ${m[1]}`,
173
+ reason: '"Kind of" is vague and informal. Use "somewhat" or be more precise.',
174
+ }),
175
+ createRegexRule({
176
+ id: 'AW_it_could',
177
+ category: 'style',
178
+ pattern: /\bit\s+could\s+be\s+argued\s+that\b/i,
179
+ suggestion: 'One argument is that',
180
+ reason: '"It could be argued" is weak hedging. State the argument directly.',
181
+ }),
182
+ ];